@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.
- package/dist/animations.d.ts +2 -0
- package/dist/animations.d.ts.map +1 -1
- package/dist/animations.js +4 -3
- package/dist/animations.js.map +1 -1
- package/dist/debug-client.d.ts +55 -2
- package/dist/debug-client.d.ts.map +1 -1
- package/dist/debug-client.js +77 -0
- package/dist/debug-client.js.map +1 -1
- package/dist/designer-support.d.ts +29 -3
- package/dist/designer-support.d.ts.map +1 -1
- package/dist/designer-support.js +557 -29
- package/dist/designer-support.js.map +1 -1
- package/dist/designer.d.ts +19 -2
- package/dist/designer.d.ts.map +1 -1
- package/dist/designer.js +1897 -143
- package/dist/designer.js.map +1 -1
- package/dist/first-drafts.d.ts.map +1 -1
- package/dist/first-drafts.js +68 -40
- package/dist/first-drafts.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts +2 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +6 -0
- package/dist/keys.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +50 -3
- package/dist/loader.js.map +1 -1
- package/dist/phaser-types.d.ts +1 -0
- package/dist/phaser-types.d.ts.map +1 -1
- package/dist/runtime.d.ts +30 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +235 -8
- package/dist/runtime.js.map +1 -1
- package/dist/tileset-dialog.d.ts +121 -0
- package/dist/tileset-dialog.d.ts.map +1 -0
- package/dist/tileset-dialog.js +1594 -0
- package/dist/tileset-dialog.js.map +1 -0
- package/dist/tilesets.d.ts +13 -0
- package/dist/tilesets.d.ts.map +1 -0
- package/dist/tilesets.js +60 -0
- package/dist/tilesets.js.map +1 -0
- package/dist/voice-line-regeneration.d.ts +47 -0
- package/dist/voice-line-regeneration.d.ts.map +1 -0
- package/dist/voice-line-regeneration.js +185 -0
- package/dist/voice-line-regeneration.js.map +1 -0
- package/package.json +3 -2
package/dist/designer.js
CHANGED
|
@@ -1,8 +1,40 @@
|
|
|
1
|
-
import { linkedAnimationAssetIds, registerInGameDesignerPanel, resolveTargetAssetId, topLevelAiAssetIds } from "@ai-game-assets/core";
|
|
1
|
+
import { expandAiAssetIds, linkedAnimationAssetIds, registerInGameDesignerPanel, resolveTargetAssetId, topLevelAiAssetIds } from "@ai-game-assets/core";
|
|
2
2
|
import { AiAssetDebugClient } from "./debug-client.js";
|
|
3
3
|
import { ensureMissingAiAssetFirstDrafts } from "./first-drafts.js";
|
|
4
4
|
import { createLocalDerivedOption, openDeriveDialog, referenceImageForCandidate } from "./derive-dialog.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createMixedTilesetOption, isDesignerTilesetAsset, openTilesetBaseMixerDialog, openTilesetAnimationEditor, openTilesetAnimationMixerDialog, openTilesetEditor, tilesetAnimationForKey, resolveTilesetBaseMixCurrent, tilesetMetadataForAsset, tilesetTileGenerationOverride } from "./tileset-dialog.js";
|
|
6
|
+
import { aiTilesetAnimationTextureKey } from "./keys.js";
|
|
7
|
+
import { promotedVoiceId, regenerateAndPromoteVoiceLines, voiceLineRegenerationPlan } from "./voice-line-regeneration.js";
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the canonical, untransformed source used by Edit Tileset. A pending
|
|
10
|
+
* option is its own editing lineage: when it has no saved source metadata, its
|
|
11
|
+
* rendered image becomes the new source instead of borrowing the active
|
|
12
|
+
* version's unrelated source sheet.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveTilesetEditorInitialState(options) {
|
|
15
|
+
if (options.currentOption) {
|
|
16
|
+
const sourceSrc = options.currentOption.tilesetSourceDataUrl;
|
|
17
|
+
return {
|
|
18
|
+
sourceSrc: sourceSrc ?? options.renderedSrc,
|
|
19
|
+
initialTransforms: sourceSrc
|
|
20
|
+
? cloneTilesetTransforms(options.currentOption.tilesetTransforms)
|
|
21
|
+
: undefined
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const sourceFile = options.activeVersion?.tilesetSourceFile;
|
|
25
|
+
return {
|
|
26
|
+
sourceSrc: sourceFile
|
|
27
|
+
? options.resolveAssetUrl(sourceFile)
|
|
28
|
+
: options.renderedSrc,
|
|
29
|
+
initialTransforms: sourceFile
|
|
30
|
+
? cloneTilesetTransforms(options.activeVersion?.tilesetTransforms)
|
|
31
|
+
: undefined
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function cloneTilesetTransforms(transforms) {
|
|
35
|
+
return transforms?.map((transform) => ({ ...transform }));
|
|
36
|
+
}
|
|
37
|
+
import { assetWithGeneratedGeometry, audioGenerationOverridesFromInputs, bindKeyboardCapture, canEditGenerationFormat, createDesignerElements, effectiveGenerationFormat, ensureDesignerStyles, errorMessage, generationOverridesFromInputs, hasStyleGuide, imageSourceToDataUrl, installPromotedImageTexture, isAbortError, isAudioAsset, isSvgSource, isVoiceAsset, loadImageElement, mimeTypeFromDataUrl, normalizeAssetFormat, normalizeAudioFormat, openAnimationEditor, openAssetVersionsDialog, openAudioEditor, openFrameTouchUpEditor, openStyleGuideEditor, pickUploadFile, positiveIntegerInput, previewCurrentAsset, previewImageSource, previewOption, readableAssetName, renderAssetFolderBrowser, renderAudioPlayer, renderOptions, resolvePreviewDisplaySize, setStatus, stopStatusAnimation, styleGuideDraftFromManifest, styleGuideRequest, uploadedOptionFromFile, voiceGenerationOverridesFromInputs } from "./designer-support.js";
|
|
6
38
|
export function installAiAssetDesigner(options) {
|
|
7
39
|
ensureDesignerStyles();
|
|
8
40
|
const client = options.client ?? new AiAssetDebugClient();
|
|
@@ -12,14 +44,53 @@ export function installAiAssetDesigner(options) {
|
|
|
12
44
|
let selectedAssetId = visibleAssetIds()[0] ?? Object.keys(manifest.assets)[0];
|
|
13
45
|
let selectedTargetId = options.targetId;
|
|
14
46
|
let selectedTargetAssetId = selectedAssetId;
|
|
47
|
+
let selectedTilesetAnimationKey;
|
|
15
48
|
let selectedOption;
|
|
49
|
+
let displayedOptions;
|
|
16
50
|
let stopCurrentAnimationPreview;
|
|
17
51
|
let editedCurrentOption;
|
|
18
52
|
let previewedVersionName;
|
|
53
|
+
const pendingOptions = new Map();
|
|
19
54
|
let styleGuideDraft = styleGuideDraftFromManifest(manifest, resolveAssetUrl);
|
|
20
55
|
const formatDrafts = new Map();
|
|
56
|
+
const tilesetPromptDrafts = new Map();
|
|
57
|
+
const tilesetAnimationPromptDrafts = new Map();
|
|
58
|
+
const tilesetAnimationFrameDrafts = new Map();
|
|
59
|
+
let tilesetAnimationPromptRevision = 0;
|
|
60
|
+
let displayedTilesetAnimationOptions;
|
|
61
|
+
let stopTilesetAnimationOptionPreviews;
|
|
21
62
|
let activeGeneration;
|
|
22
63
|
let generationId = 0;
|
|
64
|
+
let panelRevision = 0;
|
|
65
|
+
let promotionId = 0;
|
|
66
|
+
let activePromotionId;
|
|
67
|
+
let activeVoiceLineRegeneration;
|
|
68
|
+
let activeFirstDrafts = options.autoFirstDrafts !== false;
|
|
69
|
+
let destroyed = false;
|
|
70
|
+
let deferredVoiceLineManifestSync = false;
|
|
71
|
+
const deferredVoiceLinePendingAssetIds = new Set();
|
|
72
|
+
let deferredVoiceLineManifestSyncPromise;
|
|
73
|
+
let voiceLineBatchControlStates;
|
|
74
|
+
let mixingTileset = false;
|
|
75
|
+
const regenerateTilesetTile = async (assetId, tile, tileset, currentTileSrc) => {
|
|
76
|
+
const guide = await styleGuideRequest(styleGuideDraft);
|
|
77
|
+
return client.generate({
|
|
78
|
+
assetId,
|
|
79
|
+
count: 3,
|
|
80
|
+
format: effectiveGenerationFormat(manifest, formatDrafts, selectedAssetId, assetId),
|
|
81
|
+
tileset,
|
|
82
|
+
styleGuide: {
|
|
83
|
+
...guide,
|
|
84
|
+
images: [
|
|
85
|
+
...guide.images,
|
|
86
|
+
{
|
|
87
|
+
name: `${assetId}-current-tile-${tile + 1}-style.png`,
|
|
88
|
+
dataUrl: currentTileSrc
|
|
89
|
+
}
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
};
|
|
23
94
|
if (!selectedAssetId) {
|
|
24
95
|
throw new Error("AI asset designer requires at least one asset.");
|
|
25
96
|
}
|
|
@@ -37,6 +108,180 @@ export function installAiAssetDesigner(options) {
|
|
|
37
108
|
const mount = options.mount ?? document.body;
|
|
38
109
|
mount.append(elements.root);
|
|
39
110
|
bindKeyboardCapture(elements.root, options.scene);
|
|
111
|
+
const setVoiceLineBatchControlsLocked = (locked) => {
|
|
112
|
+
if (locked) {
|
|
113
|
+
if (voiceLineBatchControlStates)
|
|
114
|
+
return;
|
|
115
|
+
const controls = Array.from(elements.panel.querySelectorAll("button, input, select, textarea")).filter((control) => control !== elements.regenerateAllLinesButton);
|
|
116
|
+
voiceLineBatchControlStates = controls.map((control) => [control, control.disabled]);
|
|
117
|
+
for (const control of controls)
|
|
118
|
+
control.disabled = true;
|
|
119
|
+
elements.assetBrowser.setAttribute("inert", "");
|
|
120
|
+
elements.currentPreview.setAttribute("inert", "");
|
|
121
|
+
elements.options.setAttribute("inert", "");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
for (const [control, wasDisabled] of voiceLineBatchControlStates ?? []) {
|
|
125
|
+
control.disabled = wasDisabled;
|
|
126
|
+
}
|
|
127
|
+
voiceLineBatchControlStates = undefined;
|
|
128
|
+
elements.assetBrowser.removeAttribute("inert");
|
|
129
|
+
elements.currentPreview.removeAttribute("inert");
|
|
130
|
+
elements.options.removeAttribute("inert");
|
|
131
|
+
};
|
|
132
|
+
const syncRegenerateAllLinesButton = () => {
|
|
133
|
+
if (activeVoiceLineRegeneration) {
|
|
134
|
+
const stopping = activeVoiceLineRegeneration.controller.signal.aborted;
|
|
135
|
+
elements.regenerateAllLinesButton.hidden = false;
|
|
136
|
+
elements.regenerateAllLinesButton.disabled = stopping;
|
|
137
|
+
elements.regenerateAllLinesButton.textContent = stopping
|
|
138
|
+
? "Stopping after current line..."
|
|
139
|
+
: "Stop after current line";
|
|
140
|
+
elements.regenerateAllLinesButton.title = stopping
|
|
141
|
+
? "The batch will stop after the current line is promoted."
|
|
142
|
+
: "Finish and promote the current line, then stop before generating another.";
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const selectedAsset = manifest.assets[selectedAssetId];
|
|
146
|
+
const plan = voiceLineRegenerationPlan(manifest, selectedAssetId, selectedTargetId);
|
|
147
|
+
const { baseVoiceAssetId, lineAssetIds, missingTargetLineAssetIds } = plan;
|
|
148
|
+
const isBaseVoiceView = selectedAsset?.kind === "voice" &&
|
|
149
|
+
selectedTargetAssetId === baseVoiceAssetId &&
|
|
150
|
+
!selectedTilesetAnimationKey;
|
|
151
|
+
const hasPromotedVoice = Boolean(promotedVoiceId(manifest, baseVoiceAssetId));
|
|
152
|
+
const hasPendingBaseVoice = pendingOptions.has(baseVoiceAssetId);
|
|
153
|
+
const pendingLineCount = lineAssetIds.filter((assetId) => pendingOptions.has(assetId)).length;
|
|
154
|
+
const hasAnyPendingOption = pendingOptions.size > 0;
|
|
155
|
+
elements.regenerateAllLinesButton.textContent = "Regenerate all lines";
|
|
156
|
+
elements.regenerateAllLinesButton.hidden = !isBaseVoiceView;
|
|
157
|
+
elements.regenerateAllLinesButton.disabled =
|
|
158
|
+
!isBaseVoiceView ||
|
|
159
|
+
!hasPromotedVoice ||
|
|
160
|
+
hasPendingBaseVoice ||
|
|
161
|
+
pendingLineCount > 0 ||
|
|
162
|
+
hasAnyPendingOption ||
|
|
163
|
+
missingTargetLineAssetIds.length > 0 ||
|
|
164
|
+
lineAssetIds.length === 0 ||
|
|
165
|
+
activeFirstDrafts ||
|
|
166
|
+
Boolean(activeGeneration) ||
|
|
167
|
+
activePromotionId !== undefined;
|
|
168
|
+
elements.regenerateAllLinesButton.title = !isBaseVoiceView
|
|
169
|
+
? ""
|
|
170
|
+
: hasPendingBaseVoice
|
|
171
|
+
? "Promote the selected base voice before regenerating its lines."
|
|
172
|
+
: missingTargetLineAssetIds.length > 0
|
|
173
|
+
? `Create target variants for ${missingTargetLineAssetIds.length} linked line${missingTargetLineAssetIds.length === 1 ? "" : "s"} before regenerating this target voice.`
|
|
174
|
+
: pendingLineCount > 0
|
|
175
|
+
? `Promote or revert the ${pendingLineCount} pending linked line${pendingLineCount === 1 ? "" : "s"} before regenerating all lines.`
|
|
176
|
+
: hasAnyPendingOption
|
|
177
|
+
? "Promote or revert all pending asset options before regenerating voice lines."
|
|
178
|
+
: activeFirstDrafts
|
|
179
|
+
? "Wait for automatic first drafts to finish before regenerating voice lines."
|
|
180
|
+
: !hasPromotedVoice
|
|
181
|
+
? "Promote a base voice before regenerating its lines."
|
|
182
|
+
: lineAssetIds.length === 0
|
|
183
|
+
? "This voice has no linked lines to regenerate."
|
|
184
|
+
: `Generate and promote one new option for ${lineAssetIds.length} linked line${lineAssetIds.length === 1 ? "" : "s"}.`;
|
|
185
|
+
};
|
|
186
|
+
const syncPromoteAllButton = () => {
|
|
187
|
+
elements.promoteAllButton.disabled =
|
|
188
|
+
pendingOptions.size === 0 ||
|
|
189
|
+
Boolean(activeGeneration) ||
|
|
190
|
+
activePromotionId !== undefined;
|
|
191
|
+
syncRegenerateAllLinesButton();
|
|
192
|
+
};
|
|
193
|
+
const rememberPendingOption = (assetId, option, pending = {}) => {
|
|
194
|
+
pendingOptions.set(assetId, {
|
|
195
|
+
option,
|
|
196
|
+
inheritAnimations: Boolean(pending.inheritAnimations),
|
|
197
|
+
previewedVersionName: pending.previewedVersionName,
|
|
198
|
+
tilesetAnimations: pending.tilesetAnimations,
|
|
199
|
+
animationOnlyKey: pending.animationOnlyKey
|
|
200
|
+
});
|
|
201
|
+
syncPromoteAllButton();
|
|
202
|
+
};
|
|
203
|
+
const flushDeferredVoiceLineManifest = () => {
|
|
204
|
+
if (!deferredVoiceLineManifestSync)
|
|
205
|
+
return Promise.resolve(true);
|
|
206
|
+
if ([...deferredVoiceLinePendingAssetIds].some((assetId) => pendingOptions.has(assetId))) {
|
|
207
|
+
return Promise.resolve(false);
|
|
208
|
+
}
|
|
209
|
+
if (deferredVoiceLineManifestSyncPromise) {
|
|
210
|
+
return deferredVoiceLineManifestSyncPromise;
|
|
211
|
+
}
|
|
212
|
+
deferredVoiceLineManifestSyncPromise = (async () => {
|
|
213
|
+
try {
|
|
214
|
+
const syncedManifest = await client.syncManifestModule();
|
|
215
|
+
manifest = syncedManifest;
|
|
216
|
+
deferredVoiceLineManifestSync = false;
|
|
217
|
+
deferredVoiceLinePendingAssetIds.clear();
|
|
218
|
+
if (!destroyed) {
|
|
219
|
+
try {
|
|
220
|
+
options.onManifestUpdated?.(manifest);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
setStatus(elements, `Manifest refreshed, but the live game could not adopt it. ${errorMessage(error)}`, "error");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (!destroyed) {
|
|
230
|
+
setStatus(elements, `Could not refresh the generated manifest module. ${errorMessage(error)}`, "error");
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
deferredVoiceLineManifestSyncPromise = undefined;
|
|
236
|
+
}
|
|
237
|
+
})();
|
|
238
|
+
return deferredVoiceLineManifestSyncPromise;
|
|
239
|
+
};
|
|
240
|
+
const resolveDeferredVoiceLinePendingOption = (assetId) => {
|
|
241
|
+
if (!deferredVoiceLinePendingAssetIds.delete(assetId))
|
|
242
|
+
return;
|
|
243
|
+
if (deferredVoiceLinePendingAssetIds.size === 0) {
|
|
244
|
+
void flushDeferredVoiceLineManifest();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const forgetPendingOption = (assetId) => {
|
|
248
|
+
pendingOptions.delete(assetId);
|
|
249
|
+
resolveDeferredVoiceLinePendingOption(assetId);
|
|
250
|
+
syncPromoteAllButton();
|
|
251
|
+
};
|
|
252
|
+
const hasEditableTilesetAnimationFrames = (assetId, animationKey) => {
|
|
253
|
+
const pendingFrames = pendingOptions.get(assetId)?.tilesetAnimations?.[animationKey];
|
|
254
|
+
if (pendingFrames?.length)
|
|
255
|
+
return true;
|
|
256
|
+
const asset = manifest.assets[assetId];
|
|
257
|
+
const activeVersion = asset?.versions[asset.activeVersion];
|
|
258
|
+
return Boolean(activeVersion?.tilesetAnimations?.[animationKey]?.files.length);
|
|
259
|
+
};
|
|
260
|
+
const installTilesetAnimationFrameTextures = async (frames, asset, textureKeyForFrame) => {
|
|
261
|
+
const tileset = tilesetMetadataForAsset(asset);
|
|
262
|
+
if (!tileset || !options.scene.textures.addSpriteSheet)
|
|
263
|
+
return [];
|
|
264
|
+
const images = await Promise.all(frames.map((frame) => loadImageElement(frame)));
|
|
265
|
+
const textureKeys = images.map((image, index) => {
|
|
266
|
+
const textureKey = textureKeyForFrame(index);
|
|
267
|
+
if (options.scene.textures.exists(textureKey)) {
|
|
268
|
+
options.scene.textures.remove(textureKey);
|
|
269
|
+
}
|
|
270
|
+
options.scene.textures.addSpriteSheet?.(textureKey, image, {
|
|
271
|
+
frameWidth: tileset.tileWidth,
|
|
272
|
+
frameHeight: tileset.tileHeight,
|
|
273
|
+
margin: tileset.margin,
|
|
274
|
+
spacing: tileset.spacing
|
|
275
|
+
});
|
|
276
|
+
return textureKey;
|
|
277
|
+
});
|
|
278
|
+
return textureKeys;
|
|
279
|
+
};
|
|
280
|
+
const previewTilesetAnimationFrames = async (assetId, animationKey, frames, asset) => {
|
|
281
|
+
const previewId = Date.now();
|
|
282
|
+
const textureKeys = await installTilesetAnimationFrameTextures(frames, asset, (index) => `ai-preview:${assetId}:tileset:${encodeURIComponent(animationKey)}:${index}:${previewId}`);
|
|
283
|
+
options.onTilesetAnimationPreview?.(assetId, animationKey, textureKeys, asset);
|
|
284
|
+
};
|
|
40
285
|
const applyOpenState = (isOpen) => {
|
|
41
286
|
elements.root.dataset.open = String(isOpen);
|
|
42
287
|
elements.toggle.setAttribute("aria-expanded", String(isOpen));
|
|
@@ -60,10 +305,31 @@ export function installAiAssetDesigner(options) {
|
|
|
60
305
|
}
|
|
61
306
|
};
|
|
62
307
|
const syncAnimationChoices = (assetId) => {
|
|
308
|
+
const resolvedAssetId = resolveTargetAssetId(manifest, assetId, selectedTargetId);
|
|
309
|
+
const resolvedAsset = manifest.assets[resolvedAssetId] ?? manifest.assets[assetId];
|
|
63
310
|
const asset = manifest.assets[assetId];
|
|
64
311
|
const linkedAnimations = Object.entries(asset.linkedAnimationAssets ?? {});
|
|
65
312
|
const isVoice = asset.kind === "voice";
|
|
66
313
|
elements.animationSelect.innerHTML = "";
|
|
314
|
+
if (isDesignerTilesetAsset(resolvedAsset)) {
|
|
315
|
+
const baseOption = document.createElement("option");
|
|
316
|
+
baseOption.value = "tileset:base";
|
|
317
|
+
baseOption.textContent = "Base tileset";
|
|
318
|
+
elements.animationSelect.append(baseOption);
|
|
319
|
+
for (const animation of tilesetMetadataForAsset(resolvedAsset)?.animations ?? []) {
|
|
320
|
+
const option = document.createElement("option");
|
|
321
|
+
option.value = `tileset:animation:${encodeURIComponent(animation.key)}`;
|
|
322
|
+
option.textContent = readableAssetName(animation.key);
|
|
323
|
+
elements.animationSelect.append(option);
|
|
324
|
+
}
|
|
325
|
+
selectedTargetAssetId = resolvedAssetId;
|
|
326
|
+
selectedTilesetAnimationKey = undefined;
|
|
327
|
+
elements.animationField.hidden = false;
|
|
328
|
+
elements.animationField.firstElementChild.textContent = "Tileset view";
|
|
329
|
+
elements.animationSelect.value = "tileset:base";
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
selectedTilesetAnimationKey = undefined;
|
|
67
333
|
if (asset.kind !== "collection") {
|
|
68
334
|
const baseOption = document.createElement("option");
|
|
69
335
|
baseOption.value = resolveTargetAssetId(manifest, assetId, selectedTargetId);
|
|
@@ -83,17 +349,143 @@ export function installAiAssetDesigner(options) {
|
|
|
83
349
|
: resolveTargetAssetId(manifest, assetId, selectedTargetId);
|
|
84
350
|
elements.animationSelect.value = selectedTargetAssetId;
|
|
85
351
|
};
|
|
86
|
-
const
|
|
352
|
+
const effectiveTilesetTileCount = (asset) => {
|
|
353
|
+
const tileset = tilesetMetadataForAsset(asset);
|
|
354
|
+
if (!tileset)
|
|
355
|
+
return 0;
|
|
356
|
+
const capacity = tileset.columns * tileset.rows;
|
|
357
|
+
return positiveIntegerInput(elements.frameCountInput, Math.min(tileset.tileCount ?? capacity, capacity));
|
|
358
|
+
};
|
|
359
|
+
const tilePromptDraftsForAsset = (assetId) => {
|
|
360
|
+
const existing = tilesetPromptDrafts.get(assetId);
|
|
361
|
+
if (existing)
|
|
362
|
+
return existing;
|
|
363
|
+
const configured = tilesetMetadataForAsset(manifest.assets[assetId])?.tiles
|
|
364
|
+
?.map((tile) => tile.prompt) ?? [];
|
|
365
|
+
tilesetPromptDrafts.set(assetId, configured);
|
|
366
|
+
return configured;
|
|
367
|
+
};
|
|
368
|
+
const tilesetAnimationPromptDraftKey = (assetId, animationKey) => (`${assetId}\u0000${animationKey}`);
|
|
369
|
+
const animationTilePromptDraftsForAsset = (assetId, animationKey) => {
|
|
370
|
+
const draftKey = tilesetAnimationPromptDraftKey(assetId, animationKey);
|
|
371
|
+
const existing = tilesetAnimationPromptDrafts.get(draftKey);
|
|
372
|
+
if (existing)
|
|
373
|
+
return existing;
|
|
374
|
+
const asset = manifest.assets[assetId];
|
|
375
|
+
const tileset = tilesetMetadataForAsset(asset);
|
|
376
|
+
const animation = tilesetAnimationForKey(asset, animationKey);
|
|
377
|
+
const tileCount = tileset
|
|
378
|
+
? Math.min(tileset.tileCount ?? tileset.columns * tileset.rows, tileset.columns * tileset.rows)
|
|
379
|
+
: 0;
|
|
380
|
+
const configured = animation?.tiles?.map((tile) => tile.prompt) ??
|
|
381
|
+
Array.from({ length: tileCount }, (_, tile) => {
|
|
382
|
+
const basePrompt = tileset?.tiles?.[tile]?.prompt?.trim();
|
|
383
|
+
return basePrompt
|
|
384
|
+
? `Keep this ${basePrompt} tile unchanged unless this animation needs it to move.`
|
|
385
|
+
: "Keep this tile unchanged unless this animation needs it to move.";
|
|
386
|
+
});
|
|
387
|
+
tilesetAnimationPromptDrafts.set(draftKey, configured);
|
|
388
|
+
return configured;
|
|
389
|
+
};
|
|
390
|
+
const renderTilesetPromptInputs = (assetId) => {
|
|
391
|
+
const asset = manifest.assets[assetId];
|
|
392
|
+
const tileset = tilesetMetadataForAsset(asset);
|
|
393
|
+
const isAnimation = Boolean(selectedTilesetAnimationKey);
|
|
394
|
+
const tileCount = isAnimation && tileset
|
|
395
|
+
? Math.min(tileset.tileCount ?? tileset.columns * tileset.rows, tileset.columns * tileset.rows)
|
|
396
|
+
: effectiveTilesetTileCount(asset);
|
|
397
|
+
const drafts = selectedTilesetAnimationKey
|
|
398
|
+
? animationTilePromptDraftsForAsset(assetId, selectedTilesetAnimationKey)
|
|
399
|
+
: tilePromptDraftsForAsset(assetId);
|
|
400
|
+
if (!isAnimation)
|
|
401
|
+
elements.frameCountInput.value = String(tileCount);
|
|
402
|
+
elements.tilesetPromptsList.replaceChildren();
|
|
403
|
+
const title = elements.tilesetPromptsField.querySelector(".ai-game-assets-designer__tile-prompts-title");
|
|
404
|
+
const hint = elements.tilesetPromptsField.querySelector(".ai-game-assets-designer__tile-prompts-hint");
|
|
405
|
+
if (title)
|
|
406
|
+
title.textContent = isAnimation ? "Tile animation prompts" : "Tile prompts";
|
|
407
|
+
if (hint) {
|
|
408
|
+
hint.textContent = isAnimation
|
|
409
|
+
? "Describe how every tile behaves in this animation, in exact sheet order."
|
|
410
|
+
: "Describe each tile in exact sheet order.";
|
|
411
|
+
}
|
|
412
|
+
for (let tileIndex = 0; tileIndex < tileCount; tileIndex += 1) {
|
|
413
|
+
const input = document.createElement("textarea");
|
|
414
|
+
input.rows = 3;
|
|
415
|
+
input.value = drafts[tileIndex] ?? "";
|
|
416
|
+
input.setAttribute("aria-label", `Tile ${tileIndex + 1} prompt`);
|
|
417
|
+
input.addEventListener("input", () => {
|
|
418
|
+
drafts[tileIndex] = input.value;
|
|
419
|
+
if (isAnimation && selectedTilesetAnimationKey) {
|
|
420
|
+
elements.promoteButton.disabled = true;
|
|
421
|
+
void stageTilesetAnimationPromptDraft(assetId, selectedTilesetAnimationKey);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
const label = document.createElement("label");
|
|
425
|
+
label.className = "ai-game-assets-designer__field";
|
|
426
|
+
const labelText = document.createElement("span");
|
|
427
|
+
labelText.textContent = `Tile ${tileIndex + 1}`;
|
|
428
|
+
label.append(labelText, input);
|
|
429
|
+
elements.tilesetPromptsList.append(label);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
const tilesetAnimationPromptDefinitions = (assetId, animationKey) => {
|
|
433
|
+
const tileset = tilesetMetadataForAsset(manifest.assets[assetId]);
|
|
434
|
+
if (!tileset)
|
|
435
|
+
return undefined;
|
|
436
|
+
const tileCount = Math.min(tileset.tileCount ?? tileset.columns * tileset.rows, tileset.columns * tileset.rows);
|
|
437
|
+
const prompts = animationTilePromptDraftsForAsset(assetId, animationKey)
|
|
438
|
+
.slice(0, tileCount)
|
|
439
|
+
.map((prompt) => prompt.trim());
|
|
440
|
+
if (prompts.length !== tileCount || prompts.some((prompt) => !prompt))
|
|
441
|
+
return undefined;
|
|
442
|
+
return prompts.map((prompt) => ({ prompt }));
|
|
443
|
+
};
|
|
444
|
+
const tilesetAnimationDefinitionFromInputs = (assetId, animationKey) => {
|
|
445
|
+
const animation = tilesetAnimationForKey(manifest.assets[assetId], animationKey);
|
|
446
|
+
const tiles = tilesetAnimationPromptDefinitions(assetId, animationKey);
|
|
447
|
+
if (!animation || !tiles)
|
|
448
|
+
return undefined;
|
|
449
|
+
const frameCount = positiveIntegerInput(elements.frameCountInput, animation.frameCount);
|
|
450
|
+
const defaultDelayMs = Math.round(1000 / Math.max(1, animation.frameRate));
|
|
451
|
+
return {
|
|
452
|
+
...animation,
|
|
453
|
+
frameCount,
|
|
454
|
+
tiles,
|
|
455
|
+
frameTimings: Array.from({ length: frameCount }, (_, index) => (animation.frameTimings?.[index] ?? { delayMs: defaultDelayMs }))
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
const assetWithTilesetAnimationDefinition = (asset, definition) => ({
|
|
459
|
+
...asset,
|
|
460
|
+
tileset: asset.tileset ? {
|
|
461
|
+
...asset.tileset,
|
|
462
|
+
animations: (asset.tileset.animations ?? []).map((animation) => (animation.key === definition.key ? definition : animation))
|
|
463
|
+
} : asset.tileset
|
|
464
|
+
});
|
|
465
|
+
const tilesetPromptDefinitions = (assetId, tileCount) => {
|
|
466
|
+
const prompts = tilePromptDraftsForAsset(assetId)
|
|
467
|
+
.slice(0, tileCount)
|
|
468
|
+
.map((prompt) => prompt.trim());
|
|
469
|
+
if (prompts.length !== tileCount || prompts.some((prompt) => !prompt)) {
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
return prompts.map((prompt) => ({ prompt }));
|
|
473
|
+
};
|
|
474
|
+
const syncTargetAsset = (assetId, syncOptions = {}) => {
|
|
87
475
|
const asset = manifest.assets[assetId];
|
|
88
476
|
const activeVersion = asset.versions[asset.activeVersion];
|
|
89
477
|
const isAudio = isAudioAsset(asset);
|
|
90
478
|
const isVoice = isVoiceAsset(asset);
|
|
91
479
|
const isVoiceLine = asset.kind === "voice-line";
|
|
480
|
+
const tileset = tilesetMetadataForAsset(asset);
|
|
481
|
+
const tilesetAnimation = selectedTilesetAnimationKey
|
|
482
|
+
? tilesetAnimationForKey(asset, selectedTilesetAnimationKey)
|
|
483
|
+
: undefined;
|
|
92
484
|
stopCurrentAnimationPreview?.();
|
|
93
485
|
stopCurrentAnimationPreview = undefined;
|
|
94
|
-
elements.promptInput.value = activeVersion?.prompt ?? asset.prompt;
|
|
95
|
-
elements.widthInput.value = String(asset.frameGrid?.frameWidth ?? asset.dimensions?.width ?? 1);
|
|
96
|
-
elements.heightInput.value = String(asset.frameGrid?.frameHeight ?? asset.dimensions?.height ?? 1);
|
|
486
|
+
elements.promptInput.value = tilesetAnimation?.prompt ?? activeVersion?.prompt ?? asset.prompt;
|
|
487
|
+
elements.widthInput.value = String(asset.frameGrid?.frameWidth ?? tileset?.tileWidth ?? asset.dimensions?.width ?? 1);
|
|
488
|
+
elements.heightInput.value = String(asset.frameGrid?.frameHeight ?? tileset?.tileHeight ?? asset.dimensions?.height ?? 1);
|
|
97
489
|
elements.audioFormatSelect.value = asset.audioSettings?.format ?? "mp3";
|
|
98
490
|
elements.audioDurationInput.value = String(asset.audioSettings?.durationSeconds ?? activeVersion?.durationSeconds ?? "");
|
|
99
491
|
elements.audioLoopInput.checked = Boolean(asset.audioSettings?.loop);
|
|
@@ -110,14 +502,43 @@ export function installAiAssetDesigner(options) {
|
|
|
110
502
|
elements.audioLoopField.hidden = !isAudio;
|
|
111
503
|
elements.voiceTextField.hidden = !isVoice;
|
|
112
504
|
elements.voiceTextField.firstChild.textContent = isVoiceLine ? "Text to say" : "Demo sentence";
|
|
113
|
-
elements.formatField.hidden = isAudio ||
|
|
114
|
-
|
|
505
|
+
elements.formatField.hidden = isAudio || Boolean(selectedTilesetAnimationKey) ||
|
|
506
|
+
elements.formatField.hidden;
|
|
507
|
+
const isTilesetAnimation = Boolean(tileset && selectedTilesetAnimationKey);
|
|
508
|
+
const isBaseTileset = Boolean(tileset && !selectedTilesetAnimationKey);
|
|
509
|
+
elements.promptField.hidden = Boolean(tileset);
|
|
510
|
+
elements.tilesetPromptsField.hidden = !tileset;
|
|
511
|
+
elements.widthField.firstElementChild.textContent = tileset ? "Tile width" : "Width";
|
|
512
|
+
elements.heightField.firstElementChild.textContent = tileset ? "Tile height" : "Height";
|
|
513
|
+
elements.frameCountField.firstElementChild.textContent = isTilesetAnimation
|
|
514
|
+
? "Animation frames"
|
|
515
|
+
: tileset ? "Tiles" : "Frames";
|
|
516
|
+
elements.dimensionGrid.hidden = isAudio || isTilesetAnimation;
|
|
115
517
|
syncTargetVariantLabel(assetId);
|
|
116
|
-
elements.frameCountInput.value = String(
|
|
117
|
-
|
|
518
|
+
elements.frameCountInput.value = String(isTilesetAnimation
|
|
519
|
+
? tilesetAnimation?.frameCount ?? 1
|
|
520
|
+
: asset.frameGrid?.frameCount ??
|
|
521
|
+
(asset.frameGrid
|
|
522
|
+
? asset.frameGrid.columns * asset.frameGrid.rows
|
|
523
|
+
: tileset?.tileCount ?? (tileset ? tileset.columns * tileset.rows : 1)));
|
|
524
|
+
if (tileset && !isTilesetAnimation) {
|
|
525
|
+
elements.frameCountInput.max = String(tileset.columns * tileset.rows);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
elements.frameCountInput.removeAttribute("max");
|
|
529
|
+
}
|
|
530
|
+
if (tileset) {
|
|
531
|
+
renderTilesetPromptInputs(assetId);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
elements.tilesetPromptsList.replaceChildren();
|
|
535
|
+
}
|
|
118
536
|
const activeVersionSource = activeVersion?.file ? resolveAssetUrl(activeVersion.file) : "";
|
|
119
|
-
elements.frameCountField.hidden =
|
|
120
|
-
|
|
537
|
+
elements.frameCountField.hidden = !isTilesetAnimation &&
|
|
538
|
+
(!tileset && (asset.kind === "image" || !asset.frameGrid));
|
|
539
|
+
elements.versionLabel.textContent = selectedTilesetAnimationKey
|
|
540
|
+
? `Active ${readableAssetName(assetId)} animation: ${readableAssetName(selectedTilesetAnimationKey)}`
|
|
541
|
+
: `Active ${readableAssetName(assetId)}: ${asset.activeVersion}`;
|
|
121
542
|
elements.currentImage.src = activeVersionSource;
|
|
122
543
|
if (isAudio) {
|
|
123
544
|
renderAudioPlayer({
|
|
@@ -136,29 +557,53 @@ export function installAiAssetDesigner(options) {
|
|
|
136
557
|
elements.currentAnimation.hidden = true;
|
|
137
558
|
elements.currentAudio.hidden = !isAudio;
|
|
138
559
|
elements.currentImage.hidden = isAudio;
|
|
139
|
-
elements.currentAnimationButton.textContent =
|
|
560
|
+
elements.currentAnimationButton.textContent = isTilesetAnimation
|
|
561
|
+
? "Edit animation..."
|
|
562
|
+
: "Edit...";
|
|
563
|
+
elements.currentTouchUpButton.textContent = isBaseTileset ? "Edit tileset..." : "Touch up...";
|
|
140
564
|
elements.currentRevertButton.hidden = true;
|
|
141
565
|
elements.currentPreview.classList.add("is-selected");
|
|
142
|
-
elements.currentAnimationButton.hidden =
|
|
566
|
+
elements.currentAnimationButton.hidden = isTilesetAnimation && selectedTilesetAnimationKey
|
|
567
|
+
? !hasEditableTilesetAnimationFrames(assetId, selectedTilesetAnimationKey)
|
|
568
|
+
: !activeVersion?.file || (!asset.frameGrid && !isAudio);
|
|
143
569
|
elements.currentTouchUpButton.hidden = !activeVersion?.file ||
|
|
570
|
+
Boolean(selectedTilesetAnimationKey) ||
|
|
144
571
|
isAudio ||
|
|
145
572
|
Boolean(asset.frameGrid) ||
|
|
146
573
|
normalizeAssetFormat(asset.settings?.format) === "svg" ||
|
|
147
574
|
isSvgSource(activeVersion?.file ?? "");
|
|
148
|
-
|
|
149
|
-
|
|
575
|
+
if (syncOptions.preserveOptions && displayedOptions?.assetId === assetId) {
|
|
576
|
+
renderGeneratedOptions(assetId, displayedOptions.generated);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
clearGeneratedOptions();
|
|
580
|
+
}
|
|
150
581
|
setStatus(elements, "", "idle");
|
|
151
582
|
elements.promoteButton.disabled = true;
|
|
583
|
+
elements.uploadButton.disabled = Boolean(selectedTilesetAnimationKey);
|
|
152
584
|
elements.versionsButton.disabled = Object.keys(asset.versions).length <= 1;
|
|
153
|
-
elements.deriveButton.hidden =
|
|
585
|
+
elements.deriveButton.hidden = Boolean(selectedTilesetAnimationKey) ||
|
|
586
|
+
isAudio || deriveCandidatesForAsset(assetId).length === 0;
|
|
154
587
|
selectedOption = undefined;
|
|
155
588
|
editedCurrentOption = undefined;
|
|
156
589
|
previewedVersionName = undefined;
|
|
590
|
+
const pending = pendingOptions.get(assetId);
|
|
591
|
+
if (pending) {
|
|
592
|
+
selectedOption = pending.option;
|
|
593
|
+
editedCurrentOption = pending.option;
|
|
594
|
+
previewedVersionName = pending.previewedVersionName;
|
|
595
|
+
showOptionInCurrentPreview(pending.option, "pending preview");
|
|
596
|
+
previewedVersionName = pending.previewedVersionName;
|
|
597
|
+
elements.currentPreview.classList.add("is-selected");
|
|
598
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
599
|
+
}
|
|
600
|
+
syncPromoteAllButton();
|
|
157
601
|
};
|
|
158
602
|
const showOptionInCurrentPreview = (option, label = "selected option") => {
|
|
159
603
|
const asset = manifest.assets[selectedTargetAssetId];
|
|
160
604
|
const optionAsset = assetWithGeneratedGeometry(asset, option);
|
|
161
605
|
const isAudio = isAudioAsset(asset);
|
|
606
|
+
const isTilesetAnimation = Boolean(selectedTilesetAnimationKey && isDesignerTilesetAsset(optionAsset));
|
|
162
607
|
stopCurrentAnimationPreview?.();
|
|
163
608
|
stopCurrentAnimationPreview = undefined;
|
|
164
609
|
editedCurrentOption = option;
|
|
@@ -166,10 +611,18 @@ export function installAiAssetDesigner(options) {
|
|
|
166
611
|
elements.currentPreview.hidden = false;
|
|
167
612
|
elements.currentPreview.setAttribute("aria-label", `Preview ${label} for ${readableAssetName(selectedTargetAssetId)}`);
|
|
168
613
|
elements.currentAnimation.hidden = true;
|
|
169
|
-
elements.currentAnimationButton.textContent =
|
|
170
|
-
|
|
614
|
+
elements.currentAnimationButton.textContent = isTilesetAnimation
|
|
615
|
+
? "Edit animation..."
|
|
616
|
+
: "Edit...";
|
|
617
|
+
elements.currentAnimationButton.hidden = isTilesetAnimation && selectedTilesetAnimationKey
|
|
618
|
+
? !hasEditableTilesetAnimationFrames(selectedTargetAssetId, selectedTilesetAnimationKey)
|
|
619
|
+
: !optionAsset.frameGrid && !isAudio;
|
|
620
|
+
elements.currentTouchUpButton.textContent = isDesignerTilesetAsset(optionAsset)
|
|
621
|
+
? "Edit tileset..."
|
|
622
|
+
: "Touch up...";
|
|
171
623
|
elements.currentTouchUpButton.hidden =
|
|
172
624
|
isAudio ||
|
|
625
|
+
isTilesetAnimation ||
|
|
173
626
|
Boolean(optionAsset.frameGrid) ||
|
|
174
627
|
option.mimeType === "image/svg+xml";
|
|
175
628
|
elements.currentRevertButton.hidden = false;
|
|
@@ -189,6 +642,262 @@ export function installAiAssetDesigner(options) {
|
|
|
189
642
|
elements.currentImage.alt = `${readableAssetName(selectedTargetAssetId)} ${label}`;
|
|
190
643
|
}
|
|
191
644
|
};
|
|
645
|
+
const syncMixTilesetButton = () => {
|
|
646
|
+
const session = displayedOptions;
|
|
647
|
+
const asset = session ? manifest.assets[session.assetId] : undefined;
|
|
648
|
+
const baseVisible = Boolean(session &&
|
|
649
|
+
session.assetId === selectedTargetAssetId &&
|
|
650
|
+
session.generated.length === 3 &&
|
|
651
|
+
isDesignerTilesetAsset(asset) &&
|
|
652
|
+
!selectedTilesetAnimationKey);
|
|
653
|
+
const animationSession = displayedTilesetAnimationOptions;
|
|
654
|
+
const animationVisible = Boolean(animationSession &&
|
|
655
|
+
animationSession.assetId === selectedTargetAssetId &&
|
|
656
|
+
animationSession.animationKey === selectedTilesetAnimationKey &&
|
|
657
|
+
animationSession.generated.length === 3);
|
|
658
|
+
const visible = baseVisible || animationVisible;
|
|
659
|
+
elements.mixTilesetButton.textContent = animationVisible ? "Mix tilesets" : "Mix tileset";
|
|
660
|
+
elements.mixTilesetButton.hidden = !visible;
|
|
661
|
+
elements.mixTilesetButton.disabled = !visible || Boolean(activeGeneration || activePromotionId !== undefined || mixingTileset);
|
|
662
|
+
};
|
|
663
|
+
const renderGeneratedOptions = (assetId, generated) => {
|
|
664
|
+
stopTilesetAnimationOptionPreviews?.();
|
|
665
|
+
stopTilesetAnimationOptionPreviews = undefined;
|
|
666
|
+
displayedTilesetAnimationOptions = undefined;
|
|
667
|
+
displayedOptions = {
|
|
668
|
+
assetId,
|
|
669
|
+
generated: [...generated]
|
|
670
|
+
};
|
|
671
|
+
renderOptions({
|
|
672
|
+
elements,
|
|
673
|
+
generated,
|
|
674
|
+
scene: options.scene,
|
|
675
|
+
manifest,
|
|
676
|
+
assetId,
|
|
677
|
+
designerOptions: options,
|
|
678
|
+
onPreview: options.onPreview,
|
|
679
|
+
onSelected(option) {
|
|
680
|
+
selectedOption = option;
|
|
681
|
+
rememberPendingOption(assetId, option);
|
|
682
|
+
showOptionInCurrentPreview(option);
|
|
683
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
syncMixTilesetButton();
|
|
687
|
+
};
|
|
688
|
+
const currentTilesetOption = async (assetId, definition) => {
|
|
689
|
+
const asset = manifest.assets[assetId];
|
|
690
|
+
const pending = pendingOptions.get(assetId)?.option;
|
|
691
|
+
const activeVersion = asset.versions[asset.activeVersion];
|
|
692
|
+
const dataUrl = pending?.dataUrl ??
|
|
693
|
+
await imageSourceToDataUrl(resolveAssetUrl(activeVersion.file));
|
|
694
|
+
const tilesetSourceDataUrl = pending
|
|
695
|
+
? pending.tilesetSourceDataUrl
|
|
696
|
+
: activeVersion.tilesetSourceFile
|
|
697
|
+
? await imageSourceToDataUrl(resolveAssetUrl(activeVersion.tilesetSourceFile))
|
|
698
|
+
: undefined;
|
|
699
|
+
return {
|
|
700
|
+
...(pending ?? {
|
|
701
|
+
index: -1,
|
|
702
|
+
dataUrl,
|
|
703
|
+
mimeType: mimeTypeFromDataUrl(dataUrl),
|
|
704
|
+
prompt: activeVersion.prompt ?? asset.prompt,
|
|
705
|
+
model: activeVersion.model,
|
|
706
|
+
revisedPrompt: activeVersion.revisedPrompt,
|
|
707
|
+
dimensions: asset.dimensions,
|
|
708
|
+
settings: activeVersion.settings ?? asset.settings
|
|
709
|
+
}),
|
|
710
|
+
dataUrl,
|
|
711
|
+
tilesetSourceDataUrl,
|
|
712
|
+
tilesetTransforms: tilesetSourceDataUrl
|
|
713
|
+
? cloneTilesetTransforms(pending ? pending.tilesetTransforms : activeVersion.tilesetTransforms)
|
|
714
|
+
: undefined,
|
|
715
|
+
tileset: assetWithTilesetAnimationDefinition(asset, definition).tileset
|
|
716
|
+
};
|
|
717
|
+
};
|
|
718
|
+
const activeTilesetAnimationFrames = (assetId, animationKey) => {
|
|
719
|
+
const asset = manifest.assets[assetId];
|
|
720
|
+
const activeVersion = asset?.versions[asset.activeVersion];
|
|
721
|
+
const files = activeVersion?.tilesetAnimations?.[animationKey]?.files;
|
|
722
|
+
if (!asset || !activeVersion || !files?.length) {
|
|
723
|
+
return Promise.reject(new Error(`Generate ${readableAssetName(animationKey)} animation frames before promoting its prompts.`));
|
|
724
|
+
}
|
|
725
|
+
const cacheKey = `${assetId}\u0000${asset.activeVersion}\u0000${animationKey}`;
|
|
726
|
+
const existing = tilesetAnimationFrameDrafts.get(cacheKey);
|
|
727
|
+
if (existing)
|
|
728
|
+
return existing;
|
|
729
|
+
const loading = Promise.all(files.map((file) => imageSourceToDataUrl(resolveAssetUrl(file))));
|
|
730
|
+
tilesetAnimationFrameDrafts.set(cacheKey, loading);
|
|
731
|
+
void loading.catch(() => {
|
|
732
|
+
if (tilesetAnimationFrameDrafts.get(cacheKey) === loading) {
|
|
733
|
+
tilesetAnimationFrameDrafts.delete(cacheKey);
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
return loading;
|
|
737
|
+
};
|
|
738
|
+
const promptOnlyTilesetAnimationDefinition = (assetId, animationKey, pending = pendingOptions.get(assetId)) => {
|
|
739
|
+
const tiles = tilesetAnimationPromptDefinitions(assetId, animationKey);
|
|
740
|
+
const declared = tilesetAnimationForKey(manifest.assets[assetId], animationKey);
|
|
741
|
+
const pendingDefinition = pending?.option.tileset?.animations?.find((animation) => animation.key === animationKey);
|
|
742
|
+
const source = pendingDefinition ?? declared;
|
|
743
|
+
return source && tiles ? { ...source, tiles } : undefined;
|
|
744
|
+
};
|
|
745
|
+
const stageTilesetAnimationPromptDraft = async (assetId, animationKey) => {
|
|
746
|
+
const revision = tilesetAnimationPromptRevision + 1;
|
|
747
|
+
tilesetAnimationPromptRevision = revision;
|
|
748
|
+
if (!promptOnlyTilesetAnimationDefinition(assetId, animationKey)) {
|
|
749
|
+
elements.promoteButton.disabled = true;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
const pendingBeforeLoad = pendingOptions.get(assetId);
|
|
754
|
+
const loadedFrames = pendingBeforeLoad?.tilesetAnimations?.[animationKey] ??
|
|
755
|
+
await activeTilesetAnimationFrames(assetId, animationKey);
|
|
756
|
+
if (revision !== tilesetAnimationPromptRevision ||
|
|
757
|
+
selectedTargetAssetId !== assetId ||
|
|
758
|
+
selectedTilesetAnimationKey !== animationKey)
|
|
759
|
+
return;
|
|
760
|
+
const pending = pendingOptions.get(assetId);
|
|
761
|
+
const frames = pending?.tilesetAnimations?.[animationKey] ?? loadedFrames;
|
|
762
|
+
const definition = promptOnlyTilesetAnimationDefinition(assetId, animationKey, pending);
|
|
763
|
+
if (!definition) {
|
|
764
|
+
elements.promoteButton.disabled = true;
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (frames.length !== definition.frameCount) {
|
|
768
|
+
throw new Error(`${readableAssetName(animationKey)} has ${frames.length} current frames, but ` +
|
|
769
|
+
`${definition.frameCount} are configured. Generate new frames before promoting.`);
|
|
770
|
+
}
|
|
771
|
+
const option = await currentTilesetOption(assetId, definition);
|
|
772
|
+
if (revision !== tilesetAnimationPromptRevision ||
|
|
773
|
+
selectedTargetAssetId !== assetId ||
|
|
774
|
+
selectedTilesetAnimationKey !== animationKey)
|
|
775
|
+
return;
|
|
776
|
+
const latestPending = pendingOptions.get(assetId);
|
|
777
|
+
selectedOption = option;
|
|
778
|
+
editedCurrentOption = option;
|
|
779
|
+
rememberPendingOption(assetId, option, {
|
|
780
|
+
inheritAnimations: latestPending?.inheritAnimations,
|
|
781
|
+
previewedVersionName: latestPending?.previewedVersionName,
|
|
782
|
+
tilesetAnimations: {
|
|
783
|
+
...(latestPending?.tilesetAnimations ?? {}),
|
|
784
|
+
[animationKey]: latestPending?.tilesetAnimations?.[animationKey] ?? frames
|
|
785
|
+
},
|
|
786
|
+
animationOnlyKey: latestPending?.animationOnlyKey ??
|
|
787
|
+
(latestPending ? undefined : animationKey)
|
|
788
|
+
});
|
|
789
|
+
if (displayedTilesetAnimationOptions?.assetId === assetId &&
|
|
790
|
+
displayedTilesetAnimationOptions.animationKey === animationKey) {
|
|
791
|
+
displayedTilesetAnimationOptions.animation = {
|
|
792
|
+
...displayedTilesetAnimationOptions.animation,
|
|
793
|
+
tiles: definition.tiles
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
797
|
+
setStatus(elements, "Animation prompt changes ready. Promote to save them.", "info");
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
if (revision !== tilesetAnimationPromptRevision ||
|
|
801
|
+
selectedTargetAssetId !== assetId ||
|
|
802
|
+
selectedTilesetAnimationKey !== animationKey)
|
|
803
|
+
return;
|
|
804
|
+
elements.promoteButton.disabled = !selectedOption || activePromotionId !== undefined;
|
|
805
|
+
setStatus(elements, `Could not stage animation prompts. ${errorMessage(error)}`, "error");
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
const selectTilesetAnimationFrames = async (assetId, definition, frames, card) => {
|
|
809
|
+
const currentPrompts = tilesetAnimationPromptDefinitions(assetId, definition.key);
|
|
810
|
+
const selectedDefinition = currentPrompts
|
|
811
|
+
? { ...definition, tiles: currentPrompts }
|
|
812
|
+
: definition;
|
|
813
|
+
const option = await currentTilesetOption(assetId, selectedDefinition);
|
|
814
|
+
const pending = pendingOptions.get(assetId);
|
|
815
|
+
selectedOption = option;
|
|
816
|
+
rememberPendingOption(assetId, option, {
|
|
817
|
+
inheritAnimations: pending?.inheritAnimations,
|
|
818
|
+
previewedVersionName: pending?.previewedVersionName,
|
|
819
|
+
tilesetAnimations: {
|
|
820
|
+
...(pending?.tilesetAnimations ?? {}),
|
|
821
|
+
[selectedDefinition.key]: frames
|
|
822
|
+
},
|
|
823
|
+
animationOnlyKey: pending?.animationOnlyKey ??
|
|
824
|
+
(pending ? undefined : selectedDefinition.key)
|
|
825
|
+
});
|
|
826
|
+
for (const item of elements.options.querySelectorAll(".ai-game-assets-designer__option")) {
|
|
827
|
+
item.classList.toggle("is-selected", item === card);
|
|
828
|
+
}
|
|
829
|
+
elements.currentPreview.classList.remove("is-selected");
|
|
830
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
831
|
+
await previewTilesetAnimationFrames(assetId, selectedDefinition.key, frames, assetWithTilesetAnimationDefinition(manifest.assets[assetId], selectedDefinition));
|
|
832
|
+
};
|
|
833
|
+
const renderTilesetAnimationOptions = (assetId, definition, generated) => {
|
|
834
|
+
stopTilesetAnimationOptionPreviews?.();
|
|
835
|
+
displayedOptions = undefined;
|
|
836
|
+
displayedTilesetAnimationOptions = {
|
|
837
|
+
assetId,
|
|
838
|
+
animationKey: definition.key,
|
|
839
|
+
animation: definition,
|
|
840
|
+
generated: [...generated]
|
|
841
|
+
};
|
|
842
|
+
elements.options.replaceChildren();
|
|
843
|
+
elements.options.classList.remove("is-audio");
|
|
844
|
+
const stops = [];
|
|
845
|
+
const delayForFrame = (index) => (definition.frameTimings?.[index]?.delayMs ??
|
|
846
|
+
Math.round(1000 / Math.max(1, definition.frameRate)));
|
|
847
|
+
for (const candidate of generated) {
|
|
848
|
+
const card = document.createElement("div");
|
|
849
|
+
card.className = "ai-game-assets-designer__option";
|
|
850
|
+
const button = document.createElement("button");
|
|
851
|
+
button.type = "button";
|
|
852
|
+
button.className = "ai-game-assets-designer__option-select";
|
|
853
|
+
button.setAttribute("aria-label", `Select animation option ${candidate.index + 1}`);
|
|
854
|
+
const image = document.createElement("img");
|
|
855
|
+
image.alt = `${assetId} animation option ${candidate.index + 1}`;
|
|
856
|
+
const label = document.createElement("span");
|
|
857
|
+
label.textContent = `Option ${candidate.index + 1}`;
|
|
858
|
+
button.append(image, label);
|
|
859
|
+
card.append(button);
|
|
860
|
+
elements.options.append(card);
|
|
861
|
+
let frame = 0;
|
|
862
|
+
let timeout;
|
|
863
|
+
let stopped = false;
|
|
864
|
+
const animate = () => {
|
|
865
|
+
if (stopped || candidate.frames.length === 0)
|
|
866
|
+
return;
|
|
867
|
+
image.src = candidate.frames[frame % candidate.frames.length].dataUrl;
|
|
868
|
+
const currentFrame = frame;
|
|
869
|
+
frame += 1;
|
|
870
|
+
timeout = window.setTimeout(animate, delayForFrame(currentFrame));
|
|
871
|
+
};
|
|
872
|
+
animate();
|
|
873
|
+
stops.push(() => {
|
|
874
|
+
stopped = true;
|
|
875
|
+
if (timeout !== undefined)
|
|
876
|
+
window.clearTimeout(timeout);
|
|
877
|
+
});
|
|
878
|
+
button.addEventListener("click", async () => {
|
|
879
|
+
setStatus(elements, `Applying animation option ${candidate.index + 1}...`, "busy");
|
|
880
|
+
try {
|
|
881
|
+
await selectTilesetAnimationFrames(assetId, definition, candidate.frames.map((item) => item.dataUrl), card);
|
|
882
|
+
setStatus(elements, `Previewing animation option ${candidate.index + 1}. Promote to save it.`, "info");
|
|
883
|
+
}
|
|
884
|
+
catch (error) {
|
|
885
|
+
setStatus(elements, `Could not preview animation. ${errorMessage(error)}`, "error");
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
stopTilesetAnimationOptionPreviews = () => stops.forEach((stop) => stop());
|
|
890
|
+
syncMixTilesetButton();
|
|
891
|
+
};
|
|
892
|
+
const clearGeneratedOptions = () => {
|
|
893
|
+
stopTilesetAnimationOptionPreviews?.();
|
|
894
|
+
stopTilesetAnimationOptionPreviews = undefined;
|
|
895
|
+
displayedTilesetAnimationOptions = undefined;
|
|
896
|
+
displayedOptions = undefined;
|
|
897
|
+
elements.options.innerHTML = "";
|
|
898
|
+
elements.options.classList.remove("is-audio");
|
|
899
|
+
syncMixTilesetButton();
|
|
900
|
+
};
|
|
192
901
|
const restoreActivePreview = () => {
|
|
193
902
|
const asset = manifest.assets[selectedTargetAssetId];
|
|
194
903
|
const activeVersion = asset.versions[asset.activeVersion];
|
|
@@ -199,18 +908,28 @@ export function installAiAssetDesigner(options) {
|
|
|
199
908
|
editedCurrentOption = undefined;
|
|
200
909
|
previewedVersionName = undefined;
|
|
201
910
|
selectedOption = undefined;
|
|
911
|
+
forgetPendingOption(selectedTargetAssetId);
|
|
202
912
|
for (const item of elements.options.querySelectorAll(".ai-game-assets-designer__option")) {
|
|
203
913
|
item.classList.remove("is-selected");
|
|
204
914
|
}
|
|
205
915
|
const activeVersionSource = resolveAssetUrl(activeVersion.file);
|
|
206
916
|
const isAudio = isAudioAsset(asset);
|
|
917
|
+
const isTilesetAnimation = Boolean(selectedTilesetAnimationKey && isDesignerTilesetAsset(asset));
|
|
207
918
|
elements.currentPreview.classList.add("is-selected");
|
|
208
919
|
elements.currentPreview.setAttribute("aria-label", `Preview active ${readableAssetName(selectedTargetAssetId)} version`);
|
|
209
920
|
elements.currentAnimation.hidden = true;
|
|
210
|
-
elements.currentAnimationButton.textContent =
|
|
211
|
-
|
|
921
|
+
elements.currentAnimationButton.textContent = isTilesetAnimation
|
|
922
|
+
? "Edit animation..."
|
|
923
|
+
: "Edit...";
|
|
924
|
+
elements.currentAnimationButton.hidden = isTilesetAnimation && selectedTilesetAnimationKey
|
|
925
|
+
? !hasEditableTilesetAnimationFrames(selectedTargetAssetId, selectedTilesetAnimationKey)
|
|
926
|
+
: !asset.frameGrid && !isAudio;
|
|
927
|
+
elements.currentTouchUpButton.textContent = isDesignerTilesetAsset(asset)
|
|
928
|
+
? "Edit tileset..."
|
|
929
|
+
: "Touch up...";
|
|
212
930
|
elements.currentTouchUpButton.hidden =
|
|
213
931
|
isAudio ||
|
|
932
|
+
isTilesetAnimation ||
|
|
214
933
|
Boolean(asset.frameGrid) ||
|
|
215
934
|
normalizeAssetFormat(asset.settings?.format) === "svg" ||
|
|
216
935
|
isSvgSource(activeVersion.file);
|
|
@@ -225,21 +944,28 @@ export function installAiAssetDesigner(options) {
|
|
|
225
944
|
label: readableAssetName(selectedTargetAssetId),
|
|
226
945
|
playback: activeVersion.audioPlayback
|
|
227
946
|
});
|
|
228
|
-
options.onPreview(selectedTargetAssetId, activeVersionSource, asset);
|
|
947
|
+
(options.onAssetReady ?? options.onPreview)(selectedTargetAssetId, activeVersionSource, asset);
|
|
948
|
+
setStatus(elements, "Reverted preview to the active version.", "info");
|
|
229
949
|
}
|
|
230
950
|
else {
|
|
231
951
|
elements.currentAudio.innerHTML = "";
|
|
232
952
|
elements.currentImage.src = activeVersionSource;
|
|
233
953
|
elements.currentImage.alt = `${readableAssetName(selectedTargetAssetId)} active version`;
|
|
954
|
+
setStatus(elements, "Reverting preview in the game...", "busy");
|
|
234
955
|
previewCurrentAsset({
|
|
235
956
|
scene: options.scene,
|
|
236
957
|
manifest,
|
|
237
958
|
assetId: selectedTargetAssetId,
|
|
238
959
|
src: activeVersionSource,
|
|
239
|
-
onPreview
|
|
960
|
+
onPreview(assetId, textureKey, previewAsset) {
|
|
961
|
+
(options.onAssetReady ?? options.onPreview)(assetId, textureKey, previewAsset);
|
|
962
|
+
setStatus(elements, "Reverted preview to the active version.", "info");
|
|
963
|
+
},
|
|
964
|
+
onError(error) {
|
|
965
|
+
setStatus(elements, `Could not revert the game preview. ${error.message}`, "error");
|
|
966
|
+
}
|
|
240
967
|
});
|
|
241
968
|
}
|
|
242
|
-
setStatus(elements, "Reverted preview to the active version.", "info");
|
|
243
969
|
};
|
|
244
970
|
const syncAsset = (assetId) => {
|
|
245
971
|
syncAnimationChoices(assetId);
|
|
@@ -335,6 +1061,7 @@ export function installAiAssetDesigner(options) {
|
|
|
335
1061
|
selectedAssetId,
|
|
336
1062
|
assetIds: visibleAssetIds(),
|
|
337
1063
|
onSelect(assetId) {
|
|
1064
|
+
invalidatePanelWork();
|
|
338
1065
|
selectedAssetId = assetId;
|
|
339
1066
|
syncAsset(selectedAssetId);
|
|
340
1067
|
}
|
|
@@ -362,13 +1089,117 @@ export function installAiAssetDesigner(options) {
|
|
|
362
1089
|
activeGeneration = undefined;
|
|
363
1090
|
elements.regenerateButton.textContent = "Regenerate";
|
|
364
1091
|
unlockGenerationStatus();
|
|
1092
|
+
elements.promoteButton.disabled = !selectedOption || activePromotionId !== undefined;
|
|
1093
|
+
syncPromoteAllButton();
|
|
1094
|
+
syncMixTilesetButton();
|
|
365
1095
|
return true;
|
|
366
1096
|
};
|
|
1097
|
+
const invalidatePanelWork = () => {
|
|
1098
|
+
panelRevision += 1;
|
|
1099
|
+
if (!activeGeneration)
|
|
1100
|
+
return;
|
|
1101
|
+
const { controller, id } = activeGeneration;
|
|
1102
|
+
controller.abort();
|
|
1103
|
+
finishGeneration(id);
|
|
1104
|
+
};
|
|
1105
|
+
const regenerateTilesetAnimation = async (animationKey) => {
|
|
1106
|
+
const assetId = selectedTargetAssetId;
|
|
1107
|
+
const workflowPanelRevision = panelRevision;
|
|
1108
|
+
const isWorkflowPanelCurrent = () => panelRevision === workflowPanelRevision &&
|
|
1109
|
+
selectedTargetAssetId === assetId &&
|
|
1110
|
+
selectedTilesetAnimationKey === animationKey;
|
|
1111
|
+
const asset = manifest.assets[assetId];
|
|
1112
|
+
const animation = tilesetAnimationForKey(asset, animationKey);
|
|
1113
|
+
const activeVersion = asset?.versions[asset.activeVersion];
|
|
1114
|
+
const definition = tilesetAnimationDefinitionFromInputs(assetId, animationKey);
|
|
1115
|
+
if (!asset || !animation || !activeVersion?.file) {
|
|
1116
|
+
setStatus(elements, `Tileset animation "${animationKey}" is not ready to generate.`, "error");
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (!definition) {
|
|
1120
|
+
setStatus(elements, "Add an animation prompt for every tile before generating.", "error");
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const controller = new AbortController();
|
|
1124
|
+
const currentGenerationId = generationId + 1;
|
|
1125
|
+
generationId = currentGenerationId;
|
|
1126
|
+
activeGeneration = { controller, id: currentGenerationId };
|
|
1127
|
+
syncPromoteAllButton();
|
|
1128
|
+
elements.regenerateButton.textContent = "Cancel generation";
|
|
1129
|
+
clearGeneratedOptions();
|
|
1130
|
+
elements.promoteButton.disabled = true;
|
|
1131
|
+
setStatus(elements, "Generating options...", "busy");
|
|
1132
|
+
lockGenerationStatus();
|
|
1133
|
+
try {
|
|
1134
|
+
const candidates = await client.generateTilesetAnimationStream({
|
|
1135
|
+
assetId,
|
|
1136
|
+
animationKey,
|
|
1137
|
+
frameCount: definition.frameCount,
|
|
1138
|
+
tiles: definition.tiles,
|
|
1139
|
+
count: 3
|
|
1140
|
+
}, (candidate) => {
|
|
1141
|
+
if (activeGeneration?.id !== currentGenerationId)
|
|
1142
|
+
return;
|
|
1143
|
+
if (candidate.frames.length !== definition.frameCount) {
|
|
1144
|
+
throw new Error(`Requested ${definition.frameCount} frames, but the generation server returned ` +
|
|
1145
|
+
`${candidate.frames.length} for option ${candidate.index + 1}. ` +
|
|
1146
|
+
"Restart the AI asset dev server and regenerate the options.");
|
|
1147
|
+
}
|
|
1148
|
+
const session = displayedTilesetAnimationOptions;
|
|
1149
|
+
const generated = [
|
|
1150
|
+
...(session?.assetId === assetId && session.animationKey === animationKey
|
|
1151
|
+
? session.generated
|
|
1152
|
+
: []),
|
|
1153
|
+
candidate
|
|
1154
|
+
].sort((left, right) => left.index - right.index);
|
|
1155
|
+
renderTilesetAnimationOptions(assetId, definition, generated);
|
|
1156
|
+
setStatus(elements, `Generated option ${candidate.index + 1}. Waiting for all 3 animation sequences...`, "busy");
|
|
1157
|
+
}, {
|
|
1158
|
+
signal: controller.signal
|
|
1159
|
+
});
|
|
1160
|
+
if (!finishGeneration(currentGenerationId))
|
|
1161
|
+
return;
|
|
1162
|
+
if (candidates.length !== 3) {
|
|
1163
|
+
throw new Error(`Expected 3 tileset animation candidates, received ${candidates.length}.`);
|
|
1164
|
+
}
|
|
1165
|
+
const mismatchedCandidate = candidates.find((candidate) => (candidate.frames.length !== definition.frameCount));
|
|
1166
|
+
if (mismatchedCandidate) {
|
|
1167
|
+
throw new Error(`Requested ${definition.frameCount} frames, but option ` +
|
|
1168
|
+
`${mismatchedCandidate.index + 1} contains ${mismatchedCandidate.frames.length}. ` +
|
|
1169
|
+
"Regenerate the options for the current frame count.");
|
|
1170
|
+
}
|
|
1171
|
+
if (!isWorkflowPanelCurrent())
|
|
1172
|
+
return;
|
|
1173
|
+
renderTilesetAnimationOptions(assetId, definition, candidates);
|
|
1174
|
+
setStatus(elements, "Three animation options ready. Preview one or mix tilesets.", "info");
|
|
1175
|
+
}
|
|
1176
|
+
catch (error) {
|
|
1177
|
+
finishGeneration(currentGenerationId);
|
|
1178
|
+
if (isAbortError(error)) {
|
|
1179
|
+
if (isWorkflowPanelCurrent()) {
|
|
1180
|
+
setStatus(elements, "Generation cancelled.", "info");
|
|
1181
|
+
}
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
if (isWorkflowPanelCurrent()) {
|
|
1185
|
+
if (displayedTilesetAnimationOptions?.assetId === assetId &&
|
|
1186
|
+
displayedTilesetAnimationOptions.animationKey === animationKey) {
|
|
1187
|
+
clearGeneratedOptions();
|
|
1188
|
+
}
|
|
1189
|
+
setStatus(elements, `Tileset animation generation failed. ${errorMessage(error)}`, "error");
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
finally {
|
|
1193
|
+
finishGeneration(currentGenerationId);
|
|
1194
|
+
}
|
|
1195
|
+
};
|
|
367
1196
|
elements.assetSelect.addEventListener("change", () => {
|
|
1197
|
+
invalidatePanelWork();
|
|
368
1198
|
selectedAssetId = elements.assetSelect.value;
|
|
369
1199
|
syncAsset(selectedAssetId);
|
|
370
1200
|
});
|
|
371
1201
|
elements.targetSelect.addEventListener("change", () => {
|
|
1202
|
+
invalidatePanelWork();
|
|
372
1203
|
selectedTargetId = elements.targetSelect.value || undefined;
|
|
373
1204
|
syncAsset(selectedAssetId);
|
|
374
1205
|
setStatus(elements, selectedTargetId
|
|
@@ -376,9 +1207,27 @@ export function installAiAssetDesigner(options) {
|
|
|
376
1207
|
: "Editing default target.", "info");
|
|
377
1208
|
});
|
|
378
1209
|
elements.animationSelect.addEventListener("change", () => {
|
|
1210
|
+
invalidatePanelWork();
|
|
1211
|
+
if (isDesignerTilesetAsset(manifest.assets[selectedTargetAssetId])) {
|
|
1212
|
+
const prefix = "tileset:animation:";
|
|
1213
|
+
selectedTilesetAnimationKey = elements.animationSelect.value.startsWith(prefix)
|
|
1214
|
+
? decodeURIComponent(elements.animationSelect.value.slice(prefix.length))
|
|
1215
|
+
: undefined;
|
|
1216
|
+
syncTargetAsset(selectedTargetAssetId);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
selectedTilesetAnimationKey = undefined;
|
|
379
1220
|
selectedTargetAssetId = elements.animationSelect.value;
|
|
380
1221
|
syncTargetAsset(selectedTargetAssetId);
|
|
381
1222
|
});
|
|
1223
|
+
const syncTilesetPromptCount = () => {
|
|
1224
|
+
const asset = manifest.assets[selectedTargetAssetId];
|
|
1225
|
+
if (isDesignerTilesetAsset(asset) && !selectedTilesetAnimationKey) {
|
|
1226
|
+
renderTilesetPromptInputs(selectedTargetAssetId);
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
elements.frameCountInput.addEventListener("change", syncTilesetPromptCount);
|
|
1230
|
+
elements.frameCountInput.addEventListener("blur", syncTilesetPromptCount);
|
|
382
1231
|
elements.formatSelect.addEventListener("change", () => {
|
|
383
1232
|
if (canEditGenerationFormat(manifest, selectedAssetId, selectedTargetAssetId)) {
|
|
384
1233
|
formatDrafts.set(selectedTargetAssetId, normalizeAssetFormat(elements.formatSelect.value));
|
|
@@ -464,57 +1313,212 @@ export function installAiAssetDesigner(options) {
|
|
|
464
1313
|
event.preventDefault();
|
|
465
1314
|
elements.currentPreview.click();
|
|
466
1315
|
});
|
|
1316
|
+
elements.mixTilesetButton.addEventListener("click", async () => {
|
|
1317
|
+
const session = displayedOptions;
|
|
1318
|
+
const animationSession = displayedTilesetAnimationOptions;
|
|
1319
|
+
const assetId = selectedTargetAssetId;
|
|
1320
|
+
const asset = manifest.assets[assetId];
|
|
1321
|
+
const activeVersion = asset?.versions[asset.activeVersion];
|
|
1322
|
+
if (animationSession &&
|
|
1323
|
+
animationSession.assetId === assetId &&
|
|
1324
|
+
animationSession.animationKey === selectedTilesetAnimationKey &&
|
|
1325
|
+
animationSession.generated.length === 3 &&
|
|
1326
|
+
asset?.tileset &&
|
|
1327
|
+
activeVersion?.file &&
|
|
1328
|
+
!mixingTileset &&
|
|
1329
|
+
!activeGeneration &&
|
|
1330
|
+
activePromotionId === undefined) {
|
|
1331
|
+
const mixPanelRevision = panelRevision;
|
|
1332
|
+
const definition = animationSession.animation;
|
|
1333
|
+
const draftAsset = assetWithTilesetAnimationDefinition(asset, definition);
|
|
1334
|
+
const pending = pendingOptions.get(assetId);
|
|
1335
|
+
mixingTileset = true;
|
|
1336
|
+
syncMixTilesetButton();
|
|
1337
|
+
setStatus(elements, "Opening tileset animation mixer...", "busy");
|
|
1338
|
+
try {
|
|
1339
|
+
const baseSheetSrc = pending?.option.dataUrl ??
|
|
1340
|
+
await imageSourceToDataUrl(resolveAssetUrl(activeVersion.file));
|
|
1341
|
+
const currentFrames = pending?.tilesetAnimations?.[definition.key] ??
|
|
1342
|
+
await Promise.all((activeVersion.tilesetAnimations?.[definition.key]?.files ?? [])
|
|
1343
|
+
.map((file) => imageSourceToDataUrl(resolveAssetUrl(file))));
|
|
1344
|
+
if (panelRevision !== mixPanelRevision ||
|
|
1345
|
+
selectedTargetAssetId !== assetId ||
|
|
1346
|
+
selectedTilesetAnimationKey !== definition.key)
|
|
1347
|
+
return;
|
|
1348
|
+
const mixed = await openTilesetAnimationMixerDialog({
|
|
1349
|
+
root: elements.root,
|
|
1350
|
+
asset: draftAsset,
|
|
1351
|
+
assetId,
|
|
1352
|
+
animationKey: definition.key,
|
|
1353
|
+
baseSheetSrc,
|
|
1354
|
+
baseAnimationFrameSrcs: currentFrames,
|
|
1355
|
+
candidates: animationSession.generated,
|
|
1356
|
+
tilePrompts: definition.tiles,
|
|
1357
|
+
regenerateTile: async (tile, currentTileSrc) => {
|
|
1358
|
+
const prompt = definition.tiles?.[tile];
|
|
1359
|
+
if (!prompt)
|
|
1360
|
+
throw new Error(`Tile ${tile + 1} requires an animation prompt.`);
|
|
1361
|
+
return client.generateTilesetAnimationStream({
|
|
1362
|
+
assetId,
|
|
1363
|
+
animationKey: definition.key,
|
|
1364
|
+
count: 3,
|
|
1365
|
+
frameCount: definition.frameCount,
|
|
1366
|
+
tiles: [prompt],
|
|
1367
|
+
tileset: tilesetTileGenerationOverride(asset.tileset, tile),
|
|
1368
|
+
baseDataUrl: currentTileSrc,
|
|
1369
|
+
styleGuide: await styleGuideRequest(styleGuideDraft)
|
|
1370
|
+
}, () => undefined);
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
if (panelRevision !== mixPanelRevision ||
|
|
1374
|
+
selectedTargetAssetId !== assetId ||
|
|
1375
|
+
selectedTilesetAnimationKey !== definition.key)
|
|
1376
|
+
return;
|
|
1377
|
+
if (!mixed) {
|
|
1378
|
+
setStatus(elements, "Tileset animation mixing cancelled. The three options are still available.", "info");
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
await selectTilesetAnimationFrames(assetId, definition, mixed.frames);
|
|
1382
|
+
setStatus(elements, "Mixed tileset animation ready. Promote to save the selected tile combination.", "success");
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
setStatus(elements, `Could not mix tileset animation. ${errorMessage(error)}`, "error");
|
|
1386
|
+
}
|
|
1387
|
+
finally {
|
|
1388
|
+
mixingTileset = false;
|
|
1389
|
+
syncMixTilesetButton();
|
|
1390
|
+
}
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
if (mixingTileset ||
|
|
1394
|
+
activeGeneration ||
|
|
1395
|
+
activePromotionId !== undefined ||
|
|
1396
|
+
!session ||
|
|
1397
|
+
session.assetId !== assetId ||
|
|
1398
|
+
session.generated.length !== 3 ||
|
|
1399
|
+
!isDesignerTilesetAsset(asset) ||
|
|
1400
|
+
selectedTilesetAnimationKey ||
|
|
1401
|
+
!activeVersion?.file) {
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
const mixPanelRevision = panelRevision;
|
|
1405
|
+
const candidates = [...session.generated].sort((left, right) => left.index - right.index);
|
|
1406
|
+
mixingTileset = true;
|
|
1407
|
+
syncMixTilesetButton();
|
|
1408
|
+
setStatus(elements, "Opening tileset mixer...", "busy");
|
|
1409
|
+
try {
|
|
1410
|
+
const currentOption = editedCurrentOption;
|
|
1411
|
+
const activeSheetSrc = currentOption?.dataUrl ??
|
|
1412
|
+
await imageSourceToDataUrl(resolveAssetUrl(activeVersion.file));
|
|
1413
|
+
const current = resolveTilesetBaseMixCurrent(asset, activeSheetSrc, currentOption);
|
|
1414
|
+
if (panelRevision !== mixPanelRevision ||
|
|
1415
|
+
selectedTargetAssetId !== assetId ||
|
|
1416
|
+
selectedTilesetAnimationKey) {
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
const mixed = await openTilesetBaseMixerDialog({
|
|
1420
|
+
root: elements.root,
|
|
1421
|
+
asset: current.asset,
|
|
1422
|
+
assetId,
|
|
1423
|
+
baseSheetSrc: current.sheetSrc,
|
|
1424
|
+
candidates,
|
|
1425
|
+
regenerateTile: (tile, tileset, currentTileSrc) => regenerateTilesetTile(assetId, tile, tileset, currentTileSrc)
|
|
1426
|
+
});
|
|
1427
|
+
if (panelRevision !== mixPanelRevision ||
|
|
1428
|
+
selectedTargetAssetId !== assetId ||
|
|
1429
|
+
selectedTilesetAnimationKey) {
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
if (!mixed) {
|
|
1433
|
+
setStatus(elements, "Tileset mixing cancelled. The three options are still available.", "info");
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
const template = candidates[0];
|
|
1437
|
+
const mixedOption = createMixedTilesetOption(template, mixed, template.prompt || asset.prompt, Math.max(...candidates.map((option) => option.index)) + 1);
|
|
1438
|
+
selectedOption = mixedOption;
|
|
1439
|
+
rememberPendingOption(assetId, mixedOption);
|
|
1440
|
+
showOptionInCurrentPreview(mixedOption, "mixed tileset");
|
|
1441
|
+
elements.currentPreview.classList.add("is-selected");
|
|
1442
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
1443
|
+
previewOption({
|
|
1444
|
+
scene: options.scene,
|
|
1445
|
+
manifest,
|
|
1446
|
+
assetId,
|
|
1447
|
+
option: mixedOption,
|
|
1448
|
+
onPreview: options.onPreview
|
|
1449
|
+
});
|
|
1450
|
+
setStatus(elements, "Mixed tileset ready. Promote to save the selected tile combination.", "success");
|
|
1451
|
+
}
|
|
1452
|
+
catch (error) {
|
|
1453
|
+
setStatus(elements, `Could not mix tileset. ${errorMessage(error)}`, "error");
|
|
1454
|
+
}
|
|
1455
|
+
finally {
|
|
1456
|
+
mixingTileset = false;
|
|
1457
|
+
syncMixTilesetButton();
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
467
1460
|
elements.regenerateButton.addEventListener("click", async () => {
|
|
468
1461
|
if (activeGeneration) {
|
|
469
1462
|
activeGeneration.controller.abort();
|
|
470
1463
|
activeGeneration = undefined;
|
|
471
1464
|
elements.regenerateButton.textContent = "Regenerate";
|
|
472
1465
|
unlockGenerationStatus();
|
|
1466
|
+
elements.promoteButton.disabled = !selectedOption || activePromotionId !== undefined;
|
|
1467
|
+
syncPromoteAllButton();
|
|
473
1468
|
setStatus(elements, "Generation cancelled.", "info");
|
|
474
1469
|
return;
|
|
475
1470
|
}
|
|
1471
|
+
if (selectedTilesetAnimationKey) {
|
|
1472
|
+
await regenerateTilesetAnimation(selectedTilesetAnimationKey);
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
476
1475
|
const controller = new AbortController();
|
|
477
1476
|
const currentGenerationId = generationId + 1;
|
|
478
1477
|
const generationAssetId = selectedTargetAssetId;
|
|
1478
|
+
const generationAsset = manifest.assets[generationAssetId];
|
|
479
1479
|
const generationFormat = effectiveGenerationFormat(manifest, formatDrafts, selectedAssetId, generationAssetId);
|
|
1480
|
+
const generationOverrides = generationOverridesFromInputs(elements, generationAsset, generationFormat);
|
|
1481
|
+
const isBaseTilesetGeneration = isDesignerTilesetAsset(generationAsset);
|
|
1482
|
+
if (isBaseTilesetGeneration && generationOverrides.tileset) {
|
|
1483
|
+
renderTilesetPromptInputs(generationAssetId);
|
|
1484
|
+
const tileCount = generationOverrides.tileset.tileCount ??
|
|
1485
|
+
effectiveTilesetTileCount(generationAsset);
|
|
1486
|
+
const tiles = tilesetPromptDefinitions(generationAssetId, tileCount);
|
|
1487
|
+
if (!tiles) {
|
|
1488
|
+
setStatus(elements, "Add a prompt for every tile before generating.", "error");
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
generationOverrides.tileset = {
|
|
1492
|
+
...generationOverrides.tileset,
|
|
1493
|
+
tiles
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
480
1496
|
generationId = currentGenerationId;
|
|
481
1497
|
activeGeneration = { controller, id: currentGenerationId };
|
|
1498
|
+
syncPromoteAllButton();
|
|
482
1499
|
setStatus(elements, "Generating options...", "busy");
|
|
483
1500
|
lockGenerationStatus();
|
|
484
1501
|
elements.promoteButton.disabled = true;
|
|
485
1502
|
elements.regenerateButton.textContent = "Cancel";
|
|
486
|
-
|
|
487
|
-
selectedOption =
|
|
1503
|
+
clearGeneratedOptions();
|
|
1504
|
+
selectedOption = editedCurrentOption;
|
|
488
1505
|
const streamedOptions = [];
|
|
489
1506
|
try {
|
|
490
1507
|
const generationRequest = {
|
|
491
1508
|
assetId: generationAssetId,
|
|
492
|
-
prompt: elements.promptInput.value,
|
|
1509
|
+
prompt: isBaseTilesetGeneration ? undefined : elements.promptInput.value,
|
|
493
1510
|
count: options.optionCount ?? 3,
|
|
494
1511
|
format: generationFormat,
|
|
495
1512
|
audioSettings: audioGenerationOverridesFromInputs(elements, manifest.assets[generationAssetId]),
|
|
496
1513
|
voiceSettings: voiceGenerationOverridesFromInputs(elements, manifest.assets[generationAssetId]),
|
|
497
1514
|
styleGuide: await styleGuideRequest(styleGuideDraft),
|
|
498
|
-
...
|
|
1515
|
+
...generationOverrides
|
|
499
1516
|
};
|
|
500
1517
|
await client.generateStream(generationRequest, (option) => {
|
|
501
1518
|
if (activeGeneration?.id !== currentGenerationId)
|
|
502
1519
|
return;
|
|
503
1520
|
streamedOptions.push(option);
|
|
504
|
-
|
|
505
|
-
elements,
|
|
506
|
-
generated: [...streamedOptions].sort((left, right) => left.index - right.index),
|
|
507
|
-
scene: options.scene,
|
|
508
|
-
manifest,
|
|
509
|
-
assetId: generationAssetId,
|
|
510
|
-
designerOptions: options,
|
|
511
|
-
onPreview: options.onPreview,
|
|
512
|
-
onSelected(selected) {
|
|
513
|
-
selectedOption = selected;
|
|
514
|
-
showOptionInCurrentPreview(selected);
|
|
515
|
-
elements.promoteButton.disabled = false;
|
|
516
|
-
}
|
|
517
|
-
});
|
|
1521
|
+
renderGeneratedOptions(generationAssetId, [...streamedOptions].sort((left, right) => left.index - right.index));
|
|
518
1522
|
}, {
|
|
519
1523
|
signal: controller.signal
|
|
520
1524
|
});
|
|
@@ -522,7 +1526,11 @@ export function installAiAssetDesigner(options) {
|
|
|
522
1526
|
return;
|
|
523
1527
|
}
|
|
524
1528
|
finishGeneration(currentGenerationId);
|
|
525
|
-
setStatus(elements, streamedOptions.length
|
|
1529
|
+
setStatus(elements, streamedOptions.length === 3 && isBaseTilesetGeneration
|
|
1530
|
+
? "Three tileset options ready. Preview one or mix tiles."
|
|
1531
|
+
: streamedOptions.length > 0
|
|
1532
|
+
? "Pick an option to preview it."
|
|
1533
|
+
: "Generation finished with no options.", streamedOptions.length > 0 ? "info" : "error");
|
|
526
1534
|
}
|
|
527
1535
|
catch (error) {
|
|
528
1536
|
if (isAbortError(error)) {
|
|
@@ -530,7 +1538,7 @@ export function installAiAssetDesigner(options) {
|
|
|
530
1538
|
}
|
|
531
1539
|
finishGeneration(currentGenerationId);
|
|
532
1540
|
if (streamedOptions.length === 0) {
|
|
533
|
-
|
|
1541
|
+
clearGeneratedOptions();
|
|
534
1542
|
}
|
|
535
1543
|
setStatus(elements, `Generation failed. ${errorMessage(error)}`, "error");
|
|
536
1544
|
}
|
|
@@ -538,6 +1546,189 @@ export function installAiAssetDesigner(options) {
|
|
|
538
1546
|
finishGeneration(currentGenerationId);
|
|
539
1547
|
}
|
|
540
1548
|
});
|
|
1549
|
+
elements.regenerateAllLinesButton.addEventListener("click", async () => {
|
|
1550
|
+
if (activeVoiceLineRegeneration) {
|
|
1551
|
+
if (!activeVoiceLineRegeneration.controller.signal.aborted) {
|
|
1552
|
+
activeVoiceLineRegeneration.controller.abort();
|
|
1553
|
+
syncRegenerateAllLinesButton();
|
|
1554
|
+
setStatus(elements, "Stopping after the current line is promoted...", "busy");
|
|
1555
|
+
}
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
const voiceAssetId = selectedAssetId;
|
|
1559
|
+
const voice = manifest.assets[voiceAssetId];
|
|
1560
|
+
const plan = voiceLineRegenerationPlan(manifest, voiceAssetId, selectedTargetId);
|
|
1561
|
+
const { baseVoiceAssetId, lineAssetIds, missingTargetLineAssetIds } = plan;
|
|
1562
|
+
if (voice?.kind !== "voice" ||
|
|
1563
|
+
selectedTargetAssetId !== baseVoiceAssetId ||
|
|
1564
|
+
selectedTilesetAnimationKey ||
|
|
1565
|
+
activeFirstDrafts ||
|
|
1566
|
+
activeGeneration ||
|
|
1567
|
+
activePromotionId !== undefined) {
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
if (pendingOptions.has(baseVoiceAssetId)) {
|
|
1571
|
+
setStatus(elements, "Promote the selected base voice before regenerating its lines.", "error");
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (!promotedVoiceId(manifest, baseVoiceAssetId)) {
|
|
1575
|
+
setStatus(elements, "Promote a base voice before regenerating its lines.", "error");
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
if (missingTargetLineAssetIds.length > 0) {
|
|
1579
|
+
setStatus(elements, `Create target variants for ${missingTargetLineAssetIds.map(readableAssetName).join(", ")} before regenerating this target voice.`, "error");
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
if (lineAssetIds.length === 0) {
|
|
1583
|
+
setStatus(elements, "This voice has no linked lines to regenerate.", "error");
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
const pendingLineAssetIds = lineAssetIds.filter((assetId) => pendingOptions.has(assetId));
|
|
1587
|
+
if (pendingLineAssetIds.length > 0) {
|
|
1588
|
+
setStatus(elements, `Promote or revert pending options for ${pendingLineAssetIds.map(readableAssetName).join(", ")} before regenerating all lines.`, "error");
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
if (pendingOptions.size > 0) {
|
|
1592
|
+
setStatus(elements, "Promote or revert all pending asset options before regenerating voice lines.", "error");
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const targetLabel = selectedTargetId
|
|
1596
|
+
? manifest.targets?.[selectedTargetId]?.label ?? readableAssetName(selectedTargetId)
|
|
1597
|
+
: "Default";
|
|
1598
|
+
const confirmed = window.confirm(`Regenerate and promote one new option for all ${lineAssetIds.length} linked line${lineAssetIds.length === 1 ? "" : "s"} of ${readableAssetName(baseVoiceAssetId)} (${targetLabel})?\n\nThis uses paid generation and replaces each line's active version as it completes.`);
|
|
1599
|
+
if (!confirmed)
|
|
1600
|
+
return;
|
|
1601
|
+
const currentPromotionId = promotionId + 1;
|
|
1602
|
+
const controller = new AbortController();
|
|
1603
|
+
promotionId = currentPromotionId;
|
|
1604
|
+
activePromotionId = currentPromotionId;
|
|
1605
|
+
activeVoiceLineRegeneration = { controller, id: currentPromotionId };
|
|
1606
|
+
elements.panel.setAttribute("aria-busy", "true");
|
|
1607
|
+
setVoiceLineBatchControlsLocked(true);
|
|
1608
|
+
syncPromoteAllButton();
|
|
1609
|
+
syncMixTilesetButton();
|
|
1610
|
+
let promotedCount = 0;
|
|
1611
|
+
let liveRefreshError;
|
|
1612
|
+
let batchCompletedCleanly = false;
|
|
1613
|
+
try {
|
|
1614
|
+
const result = await regenerateAndPromoteVoiceLines({
|
|
1615
|
+
manifest,
|
|
1616
|
+
voiceAssetId,
|
|
1617
|
+
targetId: selectedTargetId,
|
|
1618
|
+
client,
|
|
1619
|
+
signal: controller.signal,
|
|
1620
|
+
onProgress(progress) {
|
|
1621
|
+
if (activeVoiceLineRegeneration?.id !== currentPromotionId)
|
|
1622
|
+
return;
|
|
1623
|
+
const action = controller.signal.aborted
|
|
1624
|
+
? "Finishing"
|
|
1625
|
+
: progress.phase === "generation" ? "Generating" : "Promoting";
|
|
1626
|
+
setStatus(elements, `${action} ${readableAssetName(progress.assetId)} (${progress.index + 1}/${progress.total})...`, "busy");
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
manifest = result.manifest;
|
|
1630
|
+
promotedCount = result.promoted.length;
|
|
1631
|
+
if (destroyed || activeVoiceLineRegeneration?.id !== currentPromotionId) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
if (result.manifestModuleSyncDeferred || result.manifestModuleSyncError) {
|
|
1635
|
+
deferredVoiceLineManifestSync = true;
|
|
1636
|
+
}
|
|
1637
|
+
if (result.manifestModuleSyncDeferred) {
|
|
1638
|
+
for (const failure of result.failures) {
|
|
1639
|
+
if (failure.option)
|
|
1640
|
+
deferredVoiceLinePendingAssetIds.add(failure.assetId);
|
|
1641
|
+
}
|
|
1642
|
+
if (result.cancelled?.option) {
|
|
1643
|
+
deferredVoiceLinePendingAssetIds.add(result.cancelled.assetId);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
for (const saved of result.promoted) {
|
|
1647
|
+
pendingOptions.delete(saved.asset.id);
|
|
1648
|
+
resolveDeferredVoiceLinePendingOption(saved.asset.id);
|
|
1649
|
+
formatDrafts.delete(saved.asset.id);
|
|
1650
|
+
try {
|
|
1651
|
+
(options.onAssetReady ?? options.onPreview)(saved.asset.id, resolveAssetUrl(saved.file), saved.asset);
|
|
1652
|
+
}
|
|
1653
|
+
catch (error) {
|
|
1654
|
+
liveRefreshError ??= { assetId: saved.asset.id, error };
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
for (const failure of result.failures) {
|
|
1658
|
+
if (failure.option) {
|
|
1659
|
+
rememberPendingOption(failure.assetId, failure.option);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
if (result.cancelled?.option) {
|
|
1663
|
+
rememberPendingOption(result.cancelled.assetId, result.cancelled.option);
|
|
1664
|
+
}
|
|
1665
|
+
batchCompletedCleanly =
|
|
1666
|
+
promotedCount > 0 &&
|
|
1667
|
+
result.failures.length === 0 &&
|
|
1668
|
+
!result.cancelled &&
|
|
1669
|
+
!result.manifestModuleSyncError &&
|
|
1670
|
+
!result.manifestModuleSyncDeferred &&
|
|
1671
|
+
pendingOptions.size === 0;
|
|
1672
|
+
if (promotedCount > 0) {
|
|
1673
|
+
try {
|
|
1674
|
+
options.onManifestUpdated?.(manifest);
|
|
1675
|
+
}
|
|
1676
|
+
catch (error) {
|
|
1677
|
+
liveRefreshError ??= { assetId: baseVoiceAssetId, error };
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
syncTargetAsset(baseVoiceAssetId);
|
|
1681
|
+
renderAssetBrowser();
|
|
1682
|
+
const firstFailure = result.failures[0];
|
|
1683
|
+
const processedCount = result.promoted.length + result.failures.length;
|
|
1684
|
+
if (result.cancelled) {
|
|
1685
|
+
setStatus(elements, `Stopped after ${processedCount}/${lineAssetIds.length} lines; ${promotedCount} promoted` +
|
|
1686
|
+
`${result.failures.length > 0 ? ` and ${result.failures.length} failed` : ""}.`, result.failures.length > 0 ? "error" : "success");
|
|
1687
|
+
}
|
|
1688
|
+
else if (firstFailure) {
|
|
1689
|
+
const unattemptedCount = lineAssetIds.length - processedCount;
|
|
1690
|
+
setStatus(elements, `Regenerated and promoted ${promotedCount}/${lineAssetIds.length} lines. ` +
|
|
1691
|
+
`${result.failures.length} failed; first failure was ${readableAssetName(firstFailure.assetId)} ` +
|
|
1692
|
+
`during ${firstFailure.phase}: ${errorMessage(firstFailure.error)}` +
|
|
1693
|
+
`${firstFailure.phase === "promotion" && unattemptedCount > 0
|
|
1694
|
+
? ` ${unattemptedCount} remaining line${unattemptedCount === 1 ? " was" : "s were"} not generated to avoid further paid requests while promotion is unavailable.`
|
|
1695
|
+
: ""}` +
|
|
1696
|
+
`${result.manifestModuleSyncDeferred
|
|
1697
|
+
? " The generated option remains selected; promote or revert it before restarting."
|
|
1698
|
+
: ""}`, "error");
|
|
1699
|
+
}
|
|
1700
|
+
else if (result.manifestModuleSyncError) {
|
|
1701
|
+
setStatus(elements, `Regenerated and promoted all ${promotedCount} lines, but the generated manifest module could not be refreshed. ${errorMessage(result.manifestModuleSyncError)}`, "error");
|
|
1702
|
+
}
|
|
1703
|
+
else if (liveRefreshError) {
|
|
1704
|
+
setStatus(elements, `Regenerated all ${promotedCount} lines, but ${readableAssetName(liveRefreshError.assetId)} ` +
|
|
1705
|
+
`could not refresh live. Restart to load it. ${errorMessage(liveRefreshError.error)}`, "error");
|
|
1706
|
+
}
|
|
1707
|
+
else {
|
|
1708
|
+
setStatus(elements, `Regenerated and promoted all ${promotedCount} lines.`, "success");
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
catch (error) {
|
|
1712
|
+
setStatus(elements, `Could not regenerate voice lines. ${errorMessage(error)}`, "error");
|
|
1713
|
+
}
|
|
1714
|
+
finally {
|
|
1715
|
+
if (activeVoiceLineRegeneration?.id === currentPromotionId) {
|
|
1716
|
+
activeVoiceLineRegeneration = undefined;
|
|
1717
|
+
}
|
|
1718
|
+
if (activePromotionId === currentPromotionId) {
|
|
1719
|
+
activePromotionId = undefined;
|
|
1720
|
+
}
|
|
1721
|
+
if (!destroyed) {
|
|
1722
|
+
elements.panel.removeAttribute("aria-busy");
|
|
1723
|
+
setVoiceLineBatchControlsLocked(false);
|
|
1724
|
+
syncPromoteAllButton();
|
|
1725
|
+
syncMixTilesetButton();
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (!destroyed && options.restartOnPromote && batchCompletedCleanly) {
|
|
1729
|
+
window.location.reload();
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
541
1732
|
elements.uploadButton.addEventListener("click", async () => {
|
|
542
1733
|
const asset = manifest.assets[selectedTargetAssetId];
|
|
543
1734
|
try {
|
|
@@ -550,23 +1741,23 @@ export function installAiAssetDesigner(options) {
|
|
|
550
1741
|
elements,
|
|
551
1742
|
prompt: elements.promptInput.value
|
|
552
1743
|
});
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
designerOptions: options,
|
|
560
|
-
onPreview: options.onPreview,
|
|
561
|
-
onSelected(option) {
|
|
562
|
-
selectedOption = option;
|
|
563
|
-
showOptionInCurrentPreview(option);
|
|
564
|
-
elements.promoteButton.disabled = false;
|
|
1744
|
+
if (isDesignerTilesetAsset(asset) && uploadedOption.tileset) {
|
|
1745
|
+
const tileCount = uploadedOption.tileset.tileCount ??
|
|
1746
|
+
uploadedOption.tileset.columns * uploadedOption.tileset.rows;
|
|
1747
|
+
const tiles = tilesetPromptDefinitions(selectedTargetAssetId, tileCount);
|
|
1748
|
+
if (!tiles) {
|
|
1749
|
+
throw new Error("Add a prompt for every tile before uploading a tileset.");
|
|
565
1750
|
}
|
|
566
|
-
|
|
1751
|
+
uploadedOption.tileset = {
|
|
1752
|
+
...uploadedOption.tileset,
|
|
1753
|
+
tiles
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
renderGeneratedOptions(selectedTargetAssetId, [uploadedOption]);
|
|
567
1757
|
selectedOption = uploadedOption;
|
|
1758
|
+
rememberPendingOption(selectedTargetAssetId, uploadedOption);
|
|
568
1759
|
showOptionInCurrentPreview(uploadedOption);
|
|
569
|
-
elements.promoteButton.disabled =
|
|
1760
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
570
1761
|
if (isAudioAsset(asset)) {
|
|
571
1762
|
options.onPreview(selectedTargetAssetId, uploadedOption.dataUrl, assetWithGeneratedGeometry(asset, uploadedOption));
|
|
572
1763
|
}
|
|
@@ -599,7 +1790,7 @@ export function installAiAssetDesigner(options) {
|
|
|
599
1790
|
prompt: elements.promptInput.value,
|
|
600
1791
|
format: generationFormat,
|
|
601
1792
|
onConfirm: async (deriveRequest) => {
|
|
602
|
-
|
|
1793
|
+
clearGeneratedOptions();
|
|
603
1794
|
elements.promoteButton.disabled = true;
|
|
604
1795
|
selectedOption = undefined;
|
|
605
1796
|
if (deriveRequest.strategy === "scale" ||
|
|
@@ -619,23 +1810,11 @@ export function installAiAssetDesigner(options) {
|
|
|
619
1810
|
mirrorX: deriveRequest.mirrorX,
|
|
620
1811
|
mirrorY: deriveRequest.mirrorY
|
|
621
1812
|
});
|
|
622
|
-
|
|
623
|
-
elements,
|
|
624
|
-
generated: [option],
|
|
625
|
-
scene: options.scene,
|
|
626
|
-
manifest,
|
|
627
|
-
assetId: derivationAssetId,
|
|
628
|
-
designerOptions: options,
|
|
629
|
-
onPreview: options.onPreview,
|
|
630
|
-
onSelected(selected) {
|
|
631
|
-
selectedOption = selected;
|
|
632
|
-
showOptionInCurrentPreview(selected);
|
|
633
|
-
elements.promoteButton.disabled = false;
|
|
634
|
-
}
|
|
635
|
-
});
|
|
1813
|
+
renderGeneratedOptions(derivationAssetId, [option]);
|
|
636
1814
|
selectedOption = option;
|
|
1815
|
+
rememberPendingOption(derivationAssetId, option);
|
|
637
1816
|
showOptionInCurrentPreview(option);
|
|
638
|
-
elements.promoteButton.disabled =
|
|
1817
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
639
1818
|
previewOption({
|
|
640
1819
|
scene: options.scene,
|
|
641
1820
|
manifest,
|
|
@@ -654,6 +1833,7 @@ export function installAiAssetDesigner(options) {
|
|
|
654
1833
|
: elements.promptInput.value;
|
|
655
1834
|
generationId = currentGenerationId;
|
|
656
1835
|
activeGeneration = { controller, id: currentGenerationId };
|
|
1836
|
+
syncPromoteAllButton();
|
|
657
1837
|
elements.regenerateButton.textContent = "Cancel";
|
|
658
1838
|
setStatus(elements, "Generating options...", "busy");
|
|
659
1839
|
lockGenerationStatus();
|
|
@@ -672,20 +1852,7 @@ export function installAiAssetDesigner(options) {
|
|
|
672
1852
|
if (activeGeneration?.id !== currentGenerationId)
|
|
673
1853
|
return;
|
|
674
1854
|
streamedOptions.push(option);
|
|
675
|
-
|
|
676
|
-
elements,
|
|
677
|
-
generated: [...streamedOptions].sort((left, right) => left.index - right.index),
|
|
678
|
-
scene: options.scene,
|
|
679
|
-
manifest,
|
|
680
|
-
assetId: derivationAssetId,
|
|
681
|
-
designerOptions: options,
|
|
682
|
-
onPreview: options.onPreview,
|
|
683
|
-
onSelected(selected) {
|
|
684
|
-
selectedOption = selected;
|
|
685
|
-
showOptionInCurrentPreview(selected);
|
|
686
|
-
elements.promoteButton.disabled = false;
|
|
687
|
-
}
|
|
688
|
-
});
|
|
1855
|
+
renderGeneratedOptions(derivationAssetId, [...streamedOptions].sort((left, right) => left.index - right.index));
|
|
689
1856
|
}, {
|
|
690
1857
|
signal: controller.signal
|
|
691
1858
|
});
|
|
@@ -705,53 +1872,364 @@ export function installAiAssetDesigner(options) {
|
|
|
705
1872
|
});
|
|
706
1873
|
});
|
|
707
1874
|
elements.promoteButton.addEventListener("click", async () => {
|
|
708
|
-
if (!selectedOption)
|
|
1875
|
+
if (!selectedOption || activePromotionId !== undefined)
|
|
709
1876
|
return;
|
|
1877
|
+
const currentPromotionId = promotionId + 1;
|
|
1878
|
+
promotionId = currentPromotionId;
|
|
1879
|
+
activePromotionId = currentPromotionId;
|
|
1880
|
+
syncPromoteAllButton();
|
|
1881
|
+
syncMixTilesetButton();
|
|
1882
|
+
const promotedAssetId = selectedTargetAssetId;
|
|
1883
|
+
const promotedOption = selectedOption;
|
|
1884
|
+
const promotedPending = pendingOptions.get(promotedAssetId);
|
|
1885
|
+
const promotionPanelRevision = panelRevision;
|
|
1886
|
+
const promotionSelectedAssetId = selectedAssetId;
|
|
1887
|
+
const promotionTargetId = selectedTargetId;
|
|
1888
|
+
const promotionAnimationKey = selectedTilesetAnimationKey;
|
|
1889
|
+
const inheritAnimations = Boolean(previewedVersionName);
|
|
1890
|
+
const isPromotionPanelCurrent = () => panelRevision === promotionPanelRevision &&
|
|
1891
|
+
selectedAssetId === promotionSelectedAssetId &&
|
|
1892
|
+
selectedTargetId === promotionTargetId &&
|
|
1893
|
+
selectedTargetAssetId === promotedAssetId &&
|
|
1894
|
+
selectedTilesetAnimationKey === promotionAnimationKey;
|
|
710
1895
|
const versionName = `promoted-${Date.now()}`;
|
|
711
1896
|
setStatus(elements, `Promoting ${versionName}...`, "busy");
|
|
712
1897
|
elements.promoteButton.disabled = true;
|
|
1898
|
+
elements.panel.setAttribute("aria-busy", "true");
|
|
1899
|
+
elements.panel.setAttribute("inert", "");
|
|
1900
|
+
let saved;
|
|
1901
|
+
let promotedAsset;
|
|
1902
|
+
let promotedVersionName;
|
|
1903
|
+
try {
|
|
1904
|
+
const animationFrames = promotionAnimationKey
|
|
1905
|
+
? promotedPending?.tilesetAnimations?.[promotionAnimationKey]
|
|
1906
|
+
: undefined;
|
|
1907
|
+
if (promotionAnimationKey &&
|
|
1908
|
+
animationFrames &&
|
|
1909
|
+
promotedPending?.animationOnlyKey === promotionAnimationKey) {
|
|
1910
|
+
const definition = promotedOption.tileset?.animations?.find((animation) => animation.key === promotionAnimationKey);
|
|
1911
|
+
const animationSaved = await client.saveTilesetAnimation({
|
|
1912
|
+
assetId: promotedAssetId,
|
|
1913
|
+
animationKey: promotionAnimationKey,
|
|
1914
|
+
frames: animationFrames,
|
|
1915
|
+
definition,
|
|
1916
|
+
versionName,
|
|
1917
|
+
notes: "Promoted from a selected or mixed tileset animation preview."
|
|
1918
|
+
});
|
|
1919
|
+
saved = animationSaved;
|
|
1920
|
+
manifest = animationSaved.manifest;
|
|
1921
|
+
promotedAsset = animationSaved.asset;
|
|
1922
|
+
promotedVersionName = animationSaved.versionName;
|
|
1923
|
+
}
|
|
1924
|
+
else {
|
|
1925
|
+
saved = await client.save({
|
|
1926
|
+
assetId: promotedAssetId,
|
|
1927
|
+
versionName,
|
|
1928
|
+
dataUrl: promotedOption.dataUrl,
|
|
1929
|
+
prompt: promotedOption.prompt,
|
|
1930
|
+
model: promotedOption.model,
|
|
1931
|
+
revisedPrompt: promotedOption.revisedPrompt,
|
|
1932
|
+
dimensions: promotedOption.dimensions,
|
|
1933
|
+
frameGrid: promotedOption.frameGrid,
|
|
1934
|
+
tileset: promotedOption.tileset,
|
|
1935
|
+
tilesetSourceDataUrl: promotedOption.tilesetSourceDataUrl,
|
|
1936
|
+
tilesetTransforms: promotedOption.tilesetTransforms,
|
|
1937
|
+
animations: assetWithGeneratedGeometry(manifest.assets[promotedAssetId], promotedOption, { inheritAnimations }).animations,
|
|
1938
|
+
settings: promotedOption.settings,
|
|
1939
|
+
audioSettings: promotedOption.audioSettings,
|
|
1940
|
+
audioPlayback: promotedOption.audioPlayback,
|
|
1941
|
+
voiceSettings: promotedOption.voiceSettings,
|
|
1942
|
+
durationSeconds: promotedOption.durationSeconds,
|
|
1943
|
+
activate: true,
|
|
1944
|
+
notes: "Promoted from the AI asset designer."
|
|
1945
|
+
});
|
|
1946
|
+
manifest = saved.manifest;
|
|
1947
|
+
promotedAsset = saved.asset;
|
|
1948
|
+
promotedVersionName = saved.versionName;
|
|
1949
|
+
for (const [animationKey, frames] of Object.entries(promotedPending?.tilesetAnimations ?? {})) {
|
|
1950
|
+
const animationSaved = await client.saveTilesetAnimation({
|
|
1951
|
+
assetId: promotedAssetId,
|
|
1952
|
+
animationKey,
|
|
1953
|
+
frames,
|
|
1954
|
+
definition: promotedOption.tileset?.animations?.find((animation) => animation.key === animationKey),
|
|
1955
|
+
notes: "Regenerated with the asset style during promotion."
|
|
1956
|
+
});
|
|
1957
|
+
manifest = animationSaved.manifest;
|
|
1958
|
+
promotedAsset = animationSaved.asset;
|
|
1959
|
+
promotedVersionName = animationSaved.versionName;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
for (const [animationKey, frames] of Object.entries(promotedPending?.tilesetAnimations ?? {})) {
|
|
1963
|
+
await installTilesetAnimationFrameTextures(frames, promotedAsset, (index) => aiTilesetAnimationTextureKey(promotedAssetId, animationKey, index));
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
catch (error) {
|
|
1967
|
+
if (activePromotionId === currentPromotionId) {
|
|
1968
|
+
activePromotionId = undefined;
|
|
1969
|
+
}
|
|
1970
|
+
syncPromoteAllButton();
|
|
1971
|
+
syncMixTilesetButton();
|
|
1972
|
+
elements.panel.removeAttribute("aria-busy");
|
|
1973
|
+
elements.panel.removeAttribute("inert");
|
|
1974
|
+
if (isPromotionPanelCurrent()) {
|
|
1975
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
1976
|
+
setStatus(elements, `Promotion failed. ${errorMessage(error)}`, "error");
|
|
1977
|
+
}
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
forgetPendingOption(promotedAssetId);
|
|
1981
|
+
if (isDesignerTilesetAsset(promotedAsset)) {
|
|
1982
|
+
tilesetPromptDrafts.set(promotedAssetId, promotedAsset.tileset?.tiles?.map((tile) => tile.prompt) ?? []);
|
|
1983
|
+
}
|
|
1984
|
+
let promotedTextureKey;
|
|
1985
|
+
let liveRefreshError;
|
|
1986
|
+
const captureLiveRefreshError = (error) => {
|
|
1987
|
+
liveRefreshError ??= error;
|
|
1988
|
+
};
|
|
1989
|
+
if (promotedAsset.activeVersion !== promotedVersionName) {
|
|
1990
|
+
captureLiveRefreshError(new Error(`Saved version "${promotedVersionName}" was not returned as the active version.`));
|
|
1991
|
+
}
|
|
1992
|
+
else if (!isAudioAsset(promotedAsset)) {
|
|
1993
|
+
try {
|
|
1994
|
+
promotedTextureKey = (await installPromotedImageTexture({
|
|
1995
|
+
scene: options.scene,
|
|
1996
|
+
manifest,
|
|
1997
|
+
assetId: promotedAssetId,
|
|
1998
|
+
src: promotedOption.dataUrl,
|
|
1999
|
+
assetOverride: promotedAsset
|
|
2000
|
+
})).textureKey;
|
|
2001
|
+
}
|
|
2002
|
+
catch (generatedSourceError) {
|
|
2003
|
+
try {
|
|
2004
|
+
promotedTextureKey = (await installPromotedImageTexture({
|
|
2005
|
+
scene: options.scene,
|
|
2006
|
+
manifest,
|
|
2007
|
+
assetId: promotedAssetId,
|
|
2008
|
+
src: resolveAssetUrl(saved.file),
|
|
2009
|
+
assetOverride: promotedAsset
|
|
2010
|
+
})).textureKey;
|
|
2011
|
+
}
|
|
2012
|
+
catch (savedSourceError) {
|
|
2013
|
+
captureLiveRefreshError(new Error(`Could not install the promoted texture. ${errorMessage(savedSourceError)}`, { cause: generatedSourceError }));
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
713
2017
|
try {
|
|
714
|
-
await client.save({
|
|
715
|
-
assetId: selectedTargetAssetId,
|
|
716
|
-
versionName,
|
|
717
|
-
dataUrl: selectedOption.dataUrl,
|
|
718
|
-
prompt: selectedOption.prompt,
|
|
719
|
-
model: selectedOption.model,
|
|
720
|
-
revisedPrompt: selectedOption.revisedPrompt,
|
|
721
|
-
dimensions: selectedOption.dimensions,
|
|
722
|
-
frameGrid: selectedOption.frameGrid,
|
|
723
|
-
animations: assetWithGeneratedGeometry(manifest.assets[selectedTargetAssetId], selectedOption, { inheritAnimations: Boolean(previewedVersionName) }).animations,
|
|
724
|
-
settings: selectedOption.settings,
|
|
725
|
-
audioSettings: selectedOption.audioSettings,
|
|
726
|
-
audioPlayback: selectedOption.audioPlayback,
|
|
727
|
-
voiceSettings: selectedOption.voiceSettings,
|
|
728
|
-
durationSeconds: selectedOption.durationSeconds,
|
|
729
|
-
activate: true,
|
|
730
|
-
notes: "Promoted from the AI asset designer."
|
|
731
|
-
});
|
|
732
|
-
manifest = await client.getManifest();
|
|
733
2018
|
options.onManifestUpdated?.(manifest);
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
2019
|
+
}
|
|
2020
|
+
catch (error) {
|
|
2021
|
+
captureLiveRefreshError(error);
|
|
2022
|
+
}
|
|
2023
|
+
try {
|
|
2024
|
+
if (promotedTextureKey) {
|
|
2025
|
+
(options.onAssetReady ?? options.onPreview)(promotedAssetId, promotedTextureKey, promotedAsset);
|
|
2026
|
+
}
|
|
2027
|
+
else if (isAudioAsset(promotedAsset)) {
|
|
2028
|
+
(options.onAssetReady ?? options.onPreview)(promotedAssetId, resolveAssetUrl(saved.file), promotedAsset);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
catch (error) {
|
|
2032
|
+
captureLiveRefreshError(error);
|
|
2033
|
+
}
|
|
2034
|
+
formatDrafts.delete(promotedAssetId);
|
|
2035
|
+
if (isPromotionPanelCurrent()) {
|
|
2036
|
+
try {
|
|
2037
|
+
syncTargetAsset(promotedAssetId, { preserveOptions: true });
|
|
2038
|
+
}
|
|
2039
|
+
catch (error) {
|
|
2040
|
+
captureLiveRefreshError(error);
|
|
2041
|
+
}
|
|
2042
|
+
if (liveRefreshError) {
|
|
2043
|
+
setStatus(elements, `Promoted ${promotedAssetId}, but the live game could not refresh. ` +
|
|
2044
|
+
`Restart to load it. ${errorMessage(liveRefreshError)}`, "error");
|
|
2045
|
+
}
|
|
2046
|
+
else {
|
|
2047
|
+
setStatus(elements, `Promoted ${promotedAssetId} to ${promotedVersionName}.`, "success");
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
if (activePromotionId === currentPromotionId) {
|
|
2051
|
+
activePromotionId = undefined;
|
|
2052
|
+
}
|
|
2053
|
+
syncPromoteAllButton();
|
|
2054
|
+
syncMixTilesetButton();
|
|
2055
|
+
elements.panel.removeAttribute("aria-busy");
|
|
2056
|
+
elements.panel.removeAttribute("inert");
|
|
2057
|
+
if (!isPromotionPanelCurrent() && selectedOption) {
|
|
2058
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
2059
|
+
}
|
|
2060
|
+
if (options.restartOnPromote) {
|
|
2061
|
+
window.location.reload();
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
elements.promoteAllButton.addEventListener("click", async () => {
|
|
2065
|
+
if (pendingOptions.size === 0 || activePromotionId !== undefined)
|
|
2066
|
+
return;
|
|
2067
|
+
const entries = [...pendingOptions.entries()];
|
|
2068
|
+
const currentPromotionId = promotionId + 1;
|
|
2069
|
+
promotionId = currentPromotionId;
|
|
2070
|
+
activePromotionId = currentPromotionId;
|
|
2071
|
+
elements.panel.setAttribute("aria-busy", "true");
|
|
2072
|
+
elements.panel.setAttribute("inert", "");
|
|
2073
|
+
elements.promoteButton.disabled = true;
|
|
2074
|
+
syncPromoteAllButton();
|
|
2075
|
+
syncMixTilesetButton();
|
|
2076
|
+
let promotedCount = 0;
|
|
2077
|
+
let promotionError;
|
|
2078
|
+
const liveRefreshErrors = [];
|
|
2079
|
+
for (const [index, [assetId, pending]] of entries.entries()) {
|
|
2080
|
+
const asset = manifest.assets[assetId];
|
|
2081
|
+
if (!asset) {
|
|
2082
|
+
promotionError = { assetId, error: new Error("Asset no longer exists in the manifest.") };
|
|
2083
|
+
break;
|
|
2084
|
+
}
|
|
2085
|
+
const versionName = `promoted-${Date.now()}-${index + 1}`;
|
|
2086
|
+
setStatus(elements, `Promoting ${readableAssetName(assetId)} (${index + 1}/${entries.length})...`, "busy");
|
|
2087
|
+
try {
|
|
2088
|
+
let saved;
|
|
2089
|
+
let promotedAsset;
|
|
2090
|
+
const animationOnlyFrames = pending.animationOnlyKey
|
|
2091
|
+
? pending.tilesetAnimations?.[pending.animationOnlyKey]
|
|
2092
|
+
: undefined;
|
|
2093
|
+
if (pending.animationOnlyKey && animationOnlyFrames) {
|
|
2094
|
+
saved = await client.saveTilesetAnimation({
|
|
2095
|
+
assetId,
|
|
2096
|
+
animationKey: pending.animationOnlyKey,
|
|
2097
|
+
frames: animationOnlyFrames,
|
|
2098
|
+
definition: pending.option.tileset?.animations?.find((animation) => animation.key === pending.animationOnlyKey),
|
|
2099
|
+
versionName,
|
|
2100
|
+
notes: "Promoted from a tileset animation preview with Promote all."
|
|
2101
|
+
});
|
|
2102
|
+
manifest = saved.manifest;
|
|
2103
|
+
promotedAsset = saved.asset;
|
|
2104
|
+
}
|
|
2105
|
+
else {
|
|
2106
|
+
saved = await client.save({
|
|
2107
|
+
assetId,
|
|
2108
|
+
versionName,
|
|
2109
|
+
dataUrl: pending.option.dataUrl,
|
|
2110
|
+
prompt: pending.option.prompt,
|
|
2111
|
+
model: pending.option.model,
|
|
2112
|
+
revisedPrompt: pending.option.revisedPrompt,
|
|
2113
|
+
dimensions: pending.option.dimensions,
|
|
2114
|
+
frameGrid: pending.option.frameGrid,
|
|
2115
|
+
tileset: pending.option.tileset,
|
|
2116
|
+
tilesetSourceDataUrl: pending.option.tilesetSourceDataUrl,
|
|
2117
|
+
tilesetTransforms: pending.option.tilesetTransforms,
|
|
2118
|
+
animations: assetWithGeneratedGeometry(asset, pending.option, {
|
|
2119
|
+
inheritAnimations: pending.inheritAnimations
|
|
2120
|
+
}).animations,
|
|
2121
|
+
settings: pending.option.settings,
|
|
2122
|
+
audioSettings: pending.option.audioSettings,
|
|
2123
|
+
audioPlayback: pending.option.audioPlayback,
|
|
2124
|
+
voiceSettings: pending.option.voiceSettings,
|
|
2125
|
+
durationSeconds: pending.option.durationSeconds,
|
|
2126
|
+
activate: true,
|
|
2127
|
+
notes: "Promoted from the AI asset designer with Promote all."
|
|
2128
|
+
});
|
|
2129
|
+
manifest = saved.manifest;
|
|
2130
|
+
promotedAsset = saved.asset;
|
|
2131
|
+
for (const [animationKey, frames] of Object.entries(pending.tilesetAnimations ?? {})) {
|
|
2132
|
+
const animationSaved = await client.saveTilesetAnimation({
|
|
2133
|
+
assetId,
|
|
2134
|
+
animationKey,
|
|
2135
|
+
frames,
|
|
2136
|
+
definition: pending.option.tileset?.animations?.find((animation) => animation.key === animationKey),
|
|
2137
|
+
notes: "Regenerated with the asset style during Promote all."
|
|
2138
|
+
});
|
|
2139
|
+
manifest = animationSaved.manifest;
|
|
2140
|
+
promotedAsset = animationSaved.asset;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
if (isDesignerTilesetAsset(promotedAsset)) {
|
|
2144
|
+
tilesetPromptDrafts.set(assetId, promotedAsset.tileset?.tiles?.map((tile) => tile.prompt) ?? []);
|
|
741
2145
|
}
|
|
2146
|
+
for (const [animationKey, frames] of Object.entries(pending.tilesetAnimations ?? {})) {
|
|
2147
|
+
await installTilesetAnimationFrameTextures(frames, promotedAsset, (frameIndex) => aiTilesetAnimationTextureKey(assetId, animationKey, frameIndex));
|
|
2148
|
+
}
|
|
2149
|
+
try {
|
|
2150
|
+
if (isAudioAsset(promotedAsset)) {
|
|
2151
|
+
(options.onAssetReady ?? options.onPreview)(assetId, resolveAssetUrl(saved.file), promotedAsset);
|
|
2152
|
+
}
|
|
2153
|
+
else {
|
|
2154
|
+
let installed;
|
|
2155
|
+
try {
|
|
2156
|
+
installed = await installPromotedImageTexture({
|
|
2157
|
+
scene: options.scene,
|
|
2158
|
+
manifest,
|
|
2159
|
+
assetId,
|
|
2160
|
+
src: pending.option.dataUrl,
|
|
2161
|
+
assetOverride: promotedAsset
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
catch {
|
|
2165
|
+
installed = await installPromotedImageTexture({
|
|
2166
|
+
scene: options.scene,
|
|
2167
|
+
manifest,
|
|
2168
|
+
assetId,
|
|
2169
|
+
src: resolveAssetUrl(saved.file),
|
|
2170
|
+
assetOverride: promotedAsset
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
(options.onAssetReady ?? options.onPreview)(assetId, installed.textureKey, promotedAsset);
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
catch (error) {
|
|
2177
|
+
liveRefreshErrors.push({ assetId, error });
|
|
2178
|
+
}
|
|
2179
|
+
pendingOptions.delete(assetId);
|
|
2180
|
+
resolveDeferredVoiceLinePendingOption(assetId);
|
|
2181
|
+
formatDrafts.delete(assetId);
|
|
2182
|
+
promotedCount += 1;
|
|
2183
|
+
}
|
|
2184
|
+
catch (error) {
|
|
2185
|
+
promotionError = { assetId, error };
|
|
2186
|
+
break;
|
|
742
2187
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
2188
|
+
}
|
|
2189
|
+
try {
|
|
2190
|
+
if (promotedCount > 0) {
|
|
2191
|
+
options.onManifestUpdated?.(manifest);
|
|
746
2192
|
}
|
|
2193
|
+
syncTargetAsset(selectedTargetAssetId);
|
|
2194
|
+
renderAssetBrowser();
|
|
747
2195
|
}
|
|
748
2196
|
catch (error) {
|
|
749
|
-
|
|
750
|
-
|
|
2197
|
+
liveRefreshErrors.push({ assetId: selectedTargetAssetId, error });
|
|
2198
|
+
}
|
|
2199
|
+
activePromotionId = undefined;
|
|
2200
|
+
elements.panel.removeAttribute("aria-busy");
|
|
2201
|
+
elements.panel.removeAttribute("inert");
|
|
2202
|
+
syncPromoteAllButton();
|
|
2203
|
+
syncMixTilesetButton();
|
|
2204
|
+
if (promotionError) {
|
|
2205
|
+
setStatus(elements, `Promoted ${promotedCount}/${entries.length} assets. ` +
|
|
2206
|
+
`${readableAssetName(promotionError.assetId)} failed: ${errorMessage(promotionError.error)}`, "error");
|
|
2207
|
+
}
|
|
2208
|
+
else if (liveRefreshErrors.length > 0) {
|
|
2209
|
+
const firstError = liveRefreshErrors[0];
|
|
2210
|
+
setStatus(elements, `Promoted all ${promotedCount} assets, but ${readableAssetName(firstError.assetId)} ` +
|
|
2211
|
+
`could not refresh live. Restart to load it. ${errorMessage(firstError.error)}`, "error");
|
|
2212
|
+
}
|
|
2213
|
+
else {
|
|
2214
|
+
setStatus(elements, `Promoted all ${promotedCount} pending assets.`, "success");
|
|
2215
|
+
}
|
|
2216
|
+
if (options.restartOnPromote && promotedCount > 0) {
|
|
2217
|
+
window.location.reload();
|
|
751
2218
|
}
|
|
752
2219
|
});
|
|
753
|
-
elements.restartButton.addEventListener("click", () => {
|
|
754
|
-
|
|
2220
|
+
elements.restartButton.addEventListener("click", async () => {
|
|
2221
|
+
if (deferredVoiceLineManifestSync &&
|
|
2222
|
+
[...deferredVoiceLinePendingAssetIds].some((assetId) => pendingOptions.has(assetId))) {
|
|
2223
|
+
setStatus(elements, "Promote or revert the pending generated voice line before restarting.", "error");
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
if (deferredVoiceLineManifestSync) {
|
|
2227
|
+
setStatus(elements, "Refreshing the generated manifest module...", "busy");
|
|
2228
|
+
if (!await flushDeferredVoiceLineManifest())
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
if (!destroyed)
|
|
2232
|
+
window.location.reload();
|
|
755
2233
|
});
|
|
756
2234
|
elements.versionsButton.addEventListener("click", () => {
|
|
757
2235
|
const asset = manifest.assets[selectedTargetAssetId];
|
|
@@ -762,30 +2240,45 @@ export function installAiAssetDesigner(options) {
|
|
|
762
2240
|
resolveAssetUrl,
|
|
763
2241
|
async onSelect(versionName, option) {
|
|
764
2242
|
const optionAsset = assetWithGeneratedGeometry(asset, option, { inheritAnimations: true });
|
|
2243
|
+
const version = asset.versions[versionName];
|
|
2244
|
+
const tilesetSourceDataUrl = version?.tilesetSourceFile
|
|
2245
|
+
? await imageSourceToDataUrl(resolveAssetUrl(version.tilesetSourceFile))
|
|
2246
|
+
: undefined;
|
|
2247
|
+
const versionOption = {
|
|
2248
|
+
...option,
|
|
2249
|
+
tilesetSourceDataUrl,
|
|
2250
|
+
tilesetTransforms: tilesetSourceDataUrl
|
|
2251
|
+
? cloneTilesetTransforms(version?.tilesetTransforms)
|
|
2252
|
+
: undefined
|
|
2253
|
+
};
|
|
765
2254
|
for (const item of elements.options.querySelectorAll(".ai-game-assets-designer__option")) {
|
|
766
2255
|
item.classList.remove("is-selected");
|
|
767
2256
|
}
|
|
768
|
-
selectedOption =
|
|
769
|
-
|
|
2257
|
+
selectedOption = versionOption;
|
|
2258
|
+
rememberPendingOption(selectedTargetAssetId, versionOption, {
|
|
2259
|
+
inheritAnimations: true,
|
|
2260
|
+
previewedVersionName: versionName
|
|
2261
|
+
});
|
|
2262
|
+
showOptionInCurrentPreview(versionOption, versionName);
|
|
770
2263
|
previewedVersionName = versionName;
|
|
771
2264
|
elements.currentPreview.classList.add("is-selected");
|
|
772
|
-
elements.promoteButton.disabled =
|
|
2265
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
773
2266
|
if (isAudioAsset(asset)) {
|
|
774
2267
|
renderAudioPlayer({
|
|
775
2268
|
container: elements.currentAudio,
|
|
776
|
-
src:
|
|
2269
|
+
src: versionOption.dataUrl,
|
|
777
2270
|
label: readableAssetName(selectedTargetAssetId),
|
|
778
|
-
playback:
|
|
2271
|
+
playback: versionOption.audioPlayback
|
|
779
2272
|
});
|
|
780
|
-
options.onPreview(selectedTargetAssetId,
|
|
2273
|
+
options.onPreview(selectedTargetAssetId, versionOption.dataUrl, optionAsset);
|
|
781
2274
|
}
|
|
782
2275
|
else {
|
|
783
|
-
elements.currentImage.src =
|
|
2276
|
+
elements.currentImage.src = versionOption.dataUrl;
|
|
784
2277
|
previewImageSource({
|
|
785
2278
|
scene: options.scene,
|
|
786
2279
|
manifest,
|
|
787
2280
|
assetId: selectedTargetAssetId,
|
|
788
|
-
src:
|
|
2281
|
+
src: versionOption.dataUrl,
|
|
789
2282
|
textureKey: `ai-version-preview:${selectedTargetAssetId}:${versionName}:${Date.now()}`,
|
|
790
2283
|
assetOverride: optionAsset,
|
|
791
2284
|
onPreview: options.onPreview
|
|
@@ -801,6 +2294,7 @@ export function installAiAssetDesigner(options) {
|
|
|
801
2294
|
});
|
|
802
2295
|
options.onManifestUpdated?.(manifest);
|
|
803
2296
|
if (previewedVersionName === versionName) {
|
|
2297
|
+
forgetPendingOption(selectedTargetAssetId);
|
|
804
2298
|
syncTargetAsset(selectedTargetAssetId);
|
|
805
2299
|
}
|
|
806
2300
|
else {
|
|
@@ -816,6 +2310,113 @@ export function installAiAssetDesigner(options) {
|
|
|
816
2310
|
}
|
|
817
2311
|
});
|
|
818
2312
|
});
|
|
2313
|
+
const previewBulkOption = (assetId, option) => new Promise((resolve, reject) => {
|
|
2314
|
+
const previewAsset = assetWithGeneratedGeometry(manifest.assets[assetId], option);
|
|
2315
|
+
previewImageSource({
|
|
2316
|
+
scene: options.scene,
|
|
2317
|
+
manifest,
|
|
2318
|
+
assetId,
|
|
2319
|
+
src: option.dataUrl,
|
|
2320
|
+
textureKey: `ai-bulk-preview:${assetId}:${Date.now()}`,
|
|
2321
|
+
assetOverride: previewAsset,
|
|
2322
|
+
onPreview(previewAssetId, textureKey, asset) {
|
|
2323
|
+
options.onPreview(previewAssetId, textureKey, asset);
|
|
2324
|
+
resolve(asset);
|
|
2325
|
+
},
|
|
2326
|
+
onError: reject
|
|
2327
|
+
});
|
|
2328
|
+
});
|
|
2329
|
+
const regenerateAllGraphicalAssets = async (styleGuide) => {
|
|
2330
|
+
if (activeGeneration || activePromotionId !== undefined)
|
|
2331
|
+
return;
|
|
2332
|
+
const assetIds = expandAiAssetIds(manifest, visibleAssetIds(), {
|
|
2333
|
+
includeLinkedAnimations: true,
|
|
2334
|
+
targetId: selectedTargetId
|
|
2335
|
+
}).filter((assetId) => {
|
|
2336
|
+
const asset = manifest.assets[assetId];
|
|
2337
|
+
return Boolean(asset && asset.kind !== "collection" && !isAudioAsset(asset));
|
|
2338
|
+
});
|
|
2339
|
+
if (assetIds.length === 0) {
|
|
2340
|
+
setStatus(elements, "There are no graphical assets to regenerate.", "info");
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
const controller = new AbortController();
|
|
2344
|
+
const currentGenerationId = generationId + 1;
|
|
2345
|
+
generationId = currentGenerationId;
|
|
2346
|
+
activeGeneration = { controller, id: currentGenerationId };
|
|
2347
|
+
clearGeneratedOptions();
|
|
2348
|
+
syncPromoteAllButton();
|
|
2349
|
+
const failures = [];
|
|
2350
|
+
let generatedCount = 0;
|
|
2351
|
+
for (const [index, assetId] of assetIds.entries()) {
|
|
2352
|
+
if (activeGeneration?.id !== currentGenerationId)
|
|
2353
|
+
return;
|
|
2354
|
+
setStatus(elements, `Regenerating ${readableAssetName(assetId)} (${index + 1}/${assetIds.length})...`, "busy");
|
|
2355
|
+
try {
|
|
2356
|
+
const generated = await client.generate({
|
|
2357
|
+
assetId,
|
|
2358
|
+
count: 1,
|
|
2359
|
+
styleGuide
|
|
2360
|
+
}, {
|
|
2361
|
+
signal: controller.signal
|
|
2362
|
+
});
|
|
2363
|
+
const option = generated[0];
|
|
2364
|
+
if (!option) {
|
|
2365
|
+
throw new Error("Generation finished without an option.");
|
|
2366
|
+
}
|
|
2367
|
+
rememberPendingOption(assetId, option);
|
|
2368
|
+
const previewAsset = await previewBulkOption(assetId, option);
|
|
2369
|
+
const generatedTilesetAnimations = {};
|
|
2370
|
+
for (const animation of previewAsset.tileset?.animations ?? []) {
|
|
2371
|
+
setStatus(elements, `Regenerating ${readableAssetName(assetId)} · ` +
|
|
2372
|
+
`${readableAssetName(animation.key)} (${index + 1}/${assetIds.length})...`, "busy");
|
|
2373
|
+
const candidates = await client.generateTilesetAnimationStream({
|
|
2374
|
+
assetId,
|
|
2375
|
+
animationKey: animation.key,
|
|
2376
|
+
prompt: animation.prompt,
|
|
2377
|
+
count: 1,
|
|
2378
|
+
baseDataUrl: option.dataUrl,
|
|
2379
|
+
styleGuide
|
|
2380
|
+
}, () => { }, {
|
|
2381
|
+
signal: controller.signal
|
|
2382
|
+
});
|
|
2383
|
+
const candidate = candidates[0];
|
|
2384
|
+
if (!candidate || candidate.frames.length !== animation.frameCount) {
|
|
2385
|
+
throw new Error(`${readableAssetName(animation.key)} did not return its ` +
|
|
2386
|
+
`${animation.frameCount} required frames.`);
|
|
2387
|
+
}
|
|
2388
|
+
const frameDataUrls = candidate.frames.map((frame) => frame.dataUrl);
|
|
2389
|
+
generatedTilesetAnimations[animation.key] = frameDataUrls;
|
|
2390
|
+
rememberPendingOption(assetId, option, {
|
|
2391
|
+
tilesetAnimations: { ...generatedTilesetAnimations }
|
|
2392
|
+
});
|
|
2393
|
+
await previewTilesetAnimationFrames(assetId, animation.key, frameDataUrls, previewAsset);
|
|
2394
|
+
}
|
|
2395
|
+
generatedCount += 1;
|
|
2396
|
+
if (selectedTargetAssetId === assetId) {
|
|
2397
|
+
selectedOption = option;
|
|
2398
|
+
showOptionInCurrentPreview(option, "regenerated preview");
|
|
2399
|
+
elements.currentPreview.classList.add("is-selected");
|
|
2400
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
catch (error) {
|
|
2404
|
+
if (isAbortError(error))
|
|
2405
|
+
return;
|
|
2406
|
+
failures.push({ assetId, error });
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
if (!finishGeneration(currentGenerationId))
|
|
2410
|
+
return;
|
|
2411
|
+
if (failures.length > 0) {
|
|
2412
|
+
const firstFailure = failures[0];
|
|
2413
|
+
setStatus(elements, `Regenerated ${generatedCount}/${assetIds.length} graphical assets. ` +
|
|
2414
|
+
`${readableAssetName(firstFailure.assetId)} failed: ${errorMessage(firstFailure.error)}`, "error");
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
setStatus(elements, `Regenerated and previewed ${generatedCount} graphical assets. ` +
|
|
2418
|
+
"Use Promote all to save them.", "success");
|
|
2419
|
+
};
|
|
819
2420
|
elements.styleButton.addEventListener("click", () => {
|
|
820
2421
|
void openStyleGuideEditor({
|
|
821
2422
|
root: elements.root,
|
|
@@ -838,16 +2439,62 @@ export function installAiAssetDesigner(options) {
|
|
|
838
2439
|
setStatus(elements, `Style promotion failed. ${errorMessage(error)}`, "error");
|
|
839
2440
|
throw error;
|
|
840
2441
|
}
|
|
2442
|
+
},
|
|
2443
|
+
async onRegenerateAll(draft) {
|
|
2444
|
+
try {
|
|
2445
|
+
await regenerateAllGraphicalAssets(await styleGuideRequest(draft));
|
|
2446
|
+
}
|
|
2447
|
+
catch (error) {
|
|
2448
|
+
setStatus(elements, `Bulk regeneration failed. ${errorMessage(error)}`, "error");
|
|
2449
|
+
}
|
|
841
2450
|
}
|
|
842
2451
|
});
|
|
843
2452
|
});
|
|
844
|
-
elements.currentAnimationButton.addEventListener("click", (event) => {
|
|
2453
|
+
elements.currentAnimationButton.addEventListener("click", async (event) => {
|
|
845
2454
|
event.stopPropagation();
|
|
846
2455
|
const asset = manifest.assets[selectedTargetAssetId];
|
|
847
2456
|
const activeVersion = asset.versions[asset.activeVersion];
|
|
848
2457
|
const sourceOption = editedCurrentOption;
|
|
849
2458
|
if (!sourceOption && !activeVersion?.file)
|
|
850
2459
|
return;
|
|
2460
|
+
if (selectedTilesetAnimationKey && isDesignerTilesetAsset(asset)) {
|
|
2461
|
+
const assetId = selectedTargetAssetId;
|
|
2462
|
+
const animationKey = selectedTilesetAnimationKey;
|
|
2463
|
+
const definition = promptOnlyTilesetAnimationDefinition(assetId, animationKey);
|
|
2464
|
+
if (!definition) {
|
|
2465
|
+
setStatus(elements, "Add an animation prompt for every tile before editing the animation.", "error");
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
try {
|
|
2469
|
+
const pendingFrames = pendingOptions.get(assetId)?.tilesetAnimations?.[animationKey];
|
|
2470
|
+
const frames = pendingFrames ?? await activeTilesetAnimationFrames(assetId, animationKey);
|
|
2471
|
+
if (frames.length !== definition.frameCount) {
|
|
2472
|
+
throw new Error(`${readableAssetName(animationKey)} has ${frames.length} current frames, but ` +
|
|
2473
|
+
`${definition.frameCount} are configured. Generate new frames before editing.`);
|
|
2474
|
+
}
|
|
2475
|
+
const editorAsset = assetWithTilesetAnimationDefinition(asset, definition);
|
|
2476
|
+
await openTilesetAnimationEditor({
|
|
2477
|
+
root: elements.root,
|
|
2478
|
+
asset: editorAsset,
|
|
2479
|
+
assetId,
|
|
2480
|
+
animationKey,
|
|
2481
|
+
frames,
|
|
2482
|
+
onConfirm: async ({ frames: editedFrames, animation }) => {
|
|
2483
|
+
const currentPrompts = tilesetAnimationPromptDefinitions(assetId, animationKey);
|
|
2484
|
+
const editedDefinition = currentPrompts
|
|
2485
|
+
? { ...animation, tiles: currentPrompts }
|
|
2486
|
+
: animation;
|
|
2487
|
+
await selectTilesetAnimationFrames(assetId, editedDefinition, editedFrames);
|
|
2488
|
+
elements.currentRevertButton.hidden = false;
|
|
2489
|
+
setStatus(elements, "Tileset animation edits applied. Promote to save them to code.", "success");
|
|
2490
|
+
}
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
catch (error) {
|
|
2494
|
+
setStatus(elements, `Could not edit tileset animation. ${errorMessage(error)}`, "error");
|
|
2495
|
+
}
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
851
2498
|
if (isAudioAsset(asset)) {
|
|
852
2499
|
void openAudioEditor({
|
|
853
2500
|
root: elements.root,
|
|
@@ -877,7 +2524,11 @@ export function installAiAssetDesigner(options) {
|
|
|
877
2524
|
durationSeconds: previousOption?.durationSeconds ?? activeVersion?.durationSeconds
|
|
878
2525
|
};
|
|
879
2526
|
selectedOption = editedCurrentOption;
|
|
880
|
-
|
|
2527
|
+
rememberPendingOption(selectedTargetAssetId, editedCurrentOption, {
|
|
2528
|
+
inheritAnimations: Boolean(previewedVersionName),
|
|
2529
|
+
previewedVersionName
|
|
2530
|
+
});
|
|
2531
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
881
2532
|
elements.currentRevertButton.hidden = false;
|
|
882
2533
|
options.onPreview(selectedTargetAssetId, dataUrl, optionAsset);
|
|
883
2534
|
renderAudioPlayer({
|
|
@@ -928,7 +2579,11 @@ export function installAiAssetDesigner(options) {
|
|
|
928
2579
|
animations
|
|
929
2580
|
};
|
|
930
2581
|
selectedOption = editedCurrentOption;
|
|
931
|
-
|
|
2582
|
+
rememberPendingOption(selectedTargetAssetId, editedCurrentOption, {
|
|
2583
|
+
inheritAnimations: Boolean(previewedVersionName),
|
|
2584
|
+
previewedVersionName
|
|
2585
|
+
});
|
|
2586
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
932
2587
|
elements.currentRevertButton.hidden = false;
|
|
933
2588
|
previewImageSource({
|
|
934
2589
|
scene: options.scene,
|
|
@@ -948,17 +2603,94 @@ export function installAiAssetDesigner(options) {
|
|
|
948
2603
|
const asset = manifest.assets[selectedTargetAssetId];
|
|
949
2604
|
const activeVersion = asset.versions[asset.activeVersion];
|
|
950
2605
|
const sourceOption = editedCurrentOption;
|
|
2606
|
+
const editorAsset = sourceOption
|
|
2607
|
+
? assetWithGeneratedGeometry(asset, sourceOption)
|
|
2608
|
+
: asset;
|
|
951
2609
|
if ((!sourceOption && !activeVersion?.file) ||
|
|
952
2610
|
isAudioAsset(asset) ||
|
|
953
|
-
|
|
2611
|
+
editorAsset.frameGrid ||
|
|
954
2612
|
sourceOption?.mimeType === "image/svg+xml" ||
|
|
955
2613
|
(!sourceOption && (normalizeAssetFormat(asset.settings?.format) === "svg" ||
|
|
956
2614
|
isSvgSource(activeVersion.file)))) {
|
|
957
2615
|
return;
|
|
958
2616
|
}
|
|
2617
|
+
if (isDesignerTilesetAsset(editorAsset)) {
|
|
2618
|
+
const assetId = selectedTargetAssetId;
|
|
2619
|
+
const editorTileset = tilesetMetadataForAsset(editorAsset);
|
|
2620
|
+
const draftTiles = tilesetPromptDefinitions(assetId, editorTileset.tileCount ?? editorTileset.columns * editorTileset.rows);
|
|
2621
|
+
const tilesetEditorAsset = draftTiles ? {
|
|
2622
|
+
...editorAsset,
|
|
2623
|
+
tileset: {
|
|
2624
|
+
...editorTileset,
|
|
2625
|
+
tiles: draftTiles
|
|
2626
|
+
}
|
|
2627
|
+
} : editorAsset;
|
|
2628
|
+
const renderedSrc = sourceOption?.dataUrl ?? resolveAssetUrl(activeVersion.file);
|
|
2629
|
+
const editorInitialState = resolveTilesetEditorInitialState({
|
|
2630
|
+
renderedSrc,
|
|
2631
|
+
currentOption: sourceOption,
|
|
2632
|
+
activeVersion,
|
|
2633
|
+
resolveAssetUrl
|
|
2634
|
+
});
|
|
2635
|
+
void openTilesetEditor({
|
|
2636
|
+
root: elements.root,
|
|
2637
|
+
asset: tilesetEditorAsset,
|
|
2638
|
+
assetId,
|
|
2639
|
+
src: renderedSrc,
|
|
2640
|
+
sourceSrc: editorInitialState.sourceSrc,
|
|
2641
|
+
initialTransforms: editorInitialState.initialTransforms,
|
|
2642
|
+
regenerateTile: (tile, tileset, currentTileSrc) => regenerateTilesetTile(assetId, tile, tileset, currentTileSrc),
|
|
2643
|
+
onConfirm: async ({ dataUrl, sourceDataUrl, transforms }) => {
|
|
2644
|
+
const previousOption = editedCurrentOption;
|
|
2645
|
+
const optionAsset = {
|
|
2646
|
+
...tilesetEditorAsset,
|
|
2647
|
+
dimensions: previousOption?.dimensions ?? tilesetEditorAsset.dimensions,
|
|
2648
|
+
tileset: tilesetEditorAsset.tileset,
|
|
2649
|
+
settings: {
|
|
2650
|
+
...(previousOption?.settings ?? activeVersion?.settings ?? tilesetEditorAsset.settings),
|
|
2651
|
+
format: "png"
|
|
2652
|
+
}
|
|
2653
|
+
};
|
|
2654
|
+
editedCurrentOption = {
|
|
2655
|
+
...previousOption,
|
|
2656
|
+
index: previousOption?.index ?? -1,
|
|
2657
|
+
dataUrl,
|
|
2658
|
+
mimeType: "image/png",
|
|
2659
|
+
prompt: previousOption?.prompt ?? activeVersion?.prompt ?? asset.prompt,
|
|
2660
|
+
model: previousOption?.model ?? activeVersion?.model,
|
|
2661
|
+
revisedPrompt: previousOption?.revisedPrompt ?? activeVersion?.revisedPrompt,
|
|
2662
|
+
dimensions: optionAsset.dimensions,
|
|
2663
|
+
tileset: optionAsset.tileset,
|
|
2664
|
+
tilesetSourceDataUrl: sourceDataUrl,
|
|
2665
|
+
tilesetTransforms: cloneTilesetTransforms(transforms),
|
|
2666
|
+
settings: optionAsset.settings
|
|
2667
|
+
};
|
|
2668
|
+
selectedOption = editedCurrentOption;
|
|
2669
|
+
rememberPendingOption(assetId, editedCurrentOption, {
|
|
2670
|
+
inheritAnimations: Boolean(previewedVersionName),
|
|
2671
|
+
previewedVersionName
|
|
2672
|
+
});
|
|
2673
|
+
elements.currentImage.src = dataUrl;
|
|
2674
|
+
elements.currentPreview.classList.add("is-selected");
|
|
2675
|
+
elements.currentRevertButton.hidden = false;
|
|
2676
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
2677
|
+
previewImageSource({
|
|
2678
|
+
scene: options.scene,
|
|
2679
|
+
manifest,
|
|
2680
|
+
assetId,
|
|
2681
|
+
src: dataUrl,
|
|
2682
|
+
textureKey: `ai-current-tileset-edit:${assetId}:${Date.now()}`,
|
|
2683
|
+
assetOverride: optionAsset,
|
|
2684
|
+
onPreview: options.onPreview
|
|
2685
|
+
});
|
|
2686
|
+
setStatus(elements, "Tileset edits applied. Promote to save them to code.", "success");
|
|
2687
|
+
}
|
|
2688
|
+
});
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
959
2691
|
void openFrameTouchUpEditor({
|
|
960
2692
|
root: elements.root,
|
|
961
|
-
asset,
|
|
2693
|
+
asset: editorAsset,
|
|
962
2694
|
title: readableAssetName(selectedTargetAssetId),
|
|
963
2695
|
frameSrc: sourceOption?.dataUrl ?? resolveAssetUrl(activeVersion.file),
|
|
964
2696
|
displaySize: resolvePreviewDisplaySize(options, selectedTargetAssetId, asset),
|
|
@@ -980,10 +2712,14 @@ export function installAiAssetDesigner(options) {
|
|
|
980
2712
|
settings: previousOption?.settings ?? activeVersion?.settings ?? asset.settings
|
|
981
2713
|
};
|
|
982
2714
|
selectedOption = editedCurrentOption;
|
|
2715
|
+
rememberPendingOption(selectedTargetAssetId, editedCurrentOption, {
|
|
2716
|
+
inheritAnimations: Boolean(previewedVersionName),
|
|
2717
|
+
previewedVersionName
|
|
2718
|
+
});
|
|
983
2719
|
elements.currentImage.src = dataUrl;
|
|
984
2720
|
elements.currentPreview.classList.add("is-selected");
|
|
985
2721
|
elements.currentRevertButton.hidden = false;
|
|
986
|
-
elements.promoteButton.disabled =
|
|
2722
|
+
elements.promoteButton.disabled = activePromotionId !== undefined;
|
|
987
2723
|
previewImageSource({
|
|
988
2724
|
scene: options.scene,
|
|
989
2725
|
manifest,
|
|
@@ -1007,22 +2743,38 @@ export function installAiAssetDesigner(options) {
|
|
|
1007
2743
|
continueOnError: true,
|
|
1008
2744
|
onManifestUpdated: (updatedManifest) => {
|
|
1009
2745
|
manifest = updatedManifest;
|
|
1010
|
-
|
|
1011
|
-
|
|
2746
|
+
if (!destroyed) {
|
|
2747
|
+
options.onManifestUpdated?.(updatedManifest);
|
|
2748
|
+
syncAsset(selectedAssetId);
|
|
2749
|
+
}
|
|
1012
2750
|
},
|
|
1013
2751
|
onAssetReady: (assetId, textureKey, asset) => {
|
|
1014
|
-
(
|
|
2752
|
+
if (!destroyed) {
|
|
2753
|
+
(options.onAssetReady ?? options.onPreview)(assetId, textureKey, asset);
|
|
2754
|
+
}
|
|
2755
|
+
},
|
|
2756
|
+
onProgress: (progress) => {
|
|
2757
|
+
if (!destroyed)
|
|
2758
|
+
handleFirstDraftProgress(progress);
|
|
1015
2759
|
},
|
|
1016
|
-
onProgress: handleFirstDraftProgress,
|
|
1017
2760
|
onError: (error, assetId) => {
|
|
1018
|
-
|
|
2761
|
+
if (!destroyed) {
|
|
2762
|
+
setStatus(elements, `First draft failed for ${readableAssetName(assetId)}: ${errorMessage(error)}`, "error");
|
|
2763
|
+
}
|
|
1019
2764
|
}
|
|
1020
2765
|
}).then((result) => {
|
|
1021
|
-
if (result.generatedAssetIds.length > 0 && result.errors.length === 0) {
|
|
2766
|
+
if (!destroyed && result.generatedAssetIds.length > 0 && result.errors.length === 0) {
|
|
1022
2767
|
setStatus(elements, "First drafts generated.", "success");
|
|
1023
2768
|
}
|
|
1024
2769
|
}).catch((error) => {
|
|
1025
|
-
|
|
2770
|
+
if (!destroyed) {
|
|
2771
|
+
setStatus(elements, `First draft failed: ${errorMessage(error)}`, "error");
|
|
2772
|
+
}
|
|
2773
|
+
}).finally(() => {
|
|
2774
|
+
activeFirstDrafts = false;
|
|
2775
|
+
if (!destroyed) {
|
|
2776
|
+
syncPromoteAllButton();
|
|
2777
|
+
}
|
|
1026
2778
|
});
|
|
1027
2779
|
}
|
|
1028
2780
|
return {
|
|
@@ -1030,7 +2782,9 @@ export function installAiAssetDesigner(options) {
|
|
|
1030
2782
|
open: () => setOpen(true),
|
|
1031
2783
|
close: () => setOpen(false),
|
|
1032
2784
|
destroy: () => {
|
|
2785
|
+
destroyed = true;
|
|
1033
2786
|
activeGeneration?.controller.abort();
|
|
2787
|
+
activeVoiceLineRegeneration?.controller.abort();
|
|
1034
2788
|
stopStatusAnimation(elements.status);
|
|
1035
2789
|
dockPanel.destroy();
|
|
1036
2790
|
elements.root.remove();
|