Fix visual novel mode for new expression thingy

5c30244627bd2184f9e126c1fc205e2e020ab0b3

Wolfsblvt <wolfsblvt@gmail.com>

1 files changed, +99 -95Showing whitespace changes
public/scripts/extensions/expressions/index.js+99 -95
@@ -12,7 +12,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
1212import { SlashCommand } from '../../slash-commands/SlashCommand.js';
1313import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
1414import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
1515import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
1616import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
1717import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
1818import { Popup, POPUP_RESULT } from '../../popup.js';
@@ -128,20 +128,20 @@ async function forceUpdateVisualNovelMode() {
128128const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, debounce_timeout.quick);
129129
130130async function updateVisualNovelMode(name, expression) {
131131 const containervnContainer = $('#visual-novel-wrapper');
132132
133133 await visualNovelRemoveInactive(containervnContainer);
134134
135135 const setSpritePromises = await visualNovelSetCharacterSprites(containervnContainer, name, expression);
136136
137137 // calculate layer indices based on recent messages
138138 await visualNovelUpdateLayers(containervnContainer);
139139
140140 await Promise.allSettled(setSpritePromises);
141141
142142 // update again based on new sprites
143143 if (setSpritePromises.length > 0) {
144144 await visualNovelUpdateLayers(containervnContainer);
145145 }
146146}
147147
@@ -172,53 +172,53 @@ async function visualNovelRemoveInactive(container) {
172172 await Promise.allSettled(removeInactiveCharactersPromises);
173173}
174174
175-async function visualNovelSetCharacterSprites(container, name, expression) {
175+/**
176+ * Sets the character sprites for visual novel mode based on the provided container, name, and expression.
177+ *
178+ * @param {JQuery<HTMLElement>} vnContainer - The container element where the sprites will be set
179+ * @param {string} spriteFolderName - The name of the sprite folder
180+ * @param {string} expression - The expression to set for the characters
181+ * @returns {Promise<Array>} - An array of promises that resolve when the sprites are set
182+ */
183+async function visualNovelSetCharacterSprites(vnContainer, spriteFolderName, expression) {
176184 const context = getContext();
177185 const group = context.groups.find(x => x.id == context.groupId);
178- const labels = await getExpressionsList();
179186
180- const createCharacterPromises = [];
181187 const setSpritePromises = [];
182188
183189 for (const avatar of group.members) {
184- const isDisabled = group.disabled_members.includes(avatar);
185-
186190 // skip disabled characters
191+ const isDisabled = group.disabled_members.includes(avatar);
187192 if (isDisabled && hideMutedSprites) {
188193 continue;
189194 }
190195
191196 const character = context.characters.find(x => x.avatar == avatar);
192-
193197 if (!character) {
194198 continue;
195199 }
196200
197- const spriteFolderName = getSpriteFolderName({ original_avatar: character.avatar }, character.name);
201+ const expressionImage = vnContainer.find(`.expression-holder[data-avatar="${avatar}"]`);
202+
203+ const memberSpriteFolderName = getSpriteFolderName({ original_avatar: character.avatar }, character.name);
198204
199205 // download images if not downloaded yet
200206 if (spriteCache[spriteFolderNamememberSpriteFolderName] === undefined) {
201207 spriteCache[spriteFolderNamememberSpriteFolderName] = await getSpritesList(spriteFolderNamememberSpriteFolderName);
202208 }
203209
204- const sprites = spriteCache[spriteFolderName];
210+ const prevExpressionSrc = expressionImage.find('img').attr('src') || null;
205- const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`);
206- const defaultExpression = getFallbackExpression();
207- // TODO: Visual novel sprites need fixing, currently do not update based on multiple sprites, etc
208- const defaultSpritePath = sprites.find(x => x.label === defaultExpression)?.path;
209- const noSprites = sprites.length === 0;
210211
211- if (expressionImage.length > 0) {
212+ const spriteFile = chooseSpriteForExpression(memberSpriteFolderName, expression, { prevExpressionSrc: prevExpressionSrc });
212213 if (name == spriteFolderNameexpressionImage.length) {
213- await validateImages(spriteFolderName, true);
214+ if (spriteFolderName == memberSpriteFolderName) {
215+ await validateImages(memberSpriteFolderName, true);
214216 setExpressionOverrideHtml(true); // <= force clear expression override input
215- const currentSpritePath = labels.includes(expression) ? sprites.find(x => x.label === expression)?.path : '';
217+ const path = spriteFile?.imageSrc || '';
216-
217- const path = currentSpritePath || defaultSpritePath || '';
218218 const img = expressionImage.find('img');
219219 await setImage(img, path);
220220 }
221221 expressionImage.toggleClass('hidden', noSprites!spriteFile);
222222 } else {
223223 const template = $('#expression-holder').clone();
224224 template.attr('id', `expression-${avatar}`);
@@ -226,18 +226,19 @@ async function visualNovelSetCharacterSprites(container, name, expression) {
226226 template.find('.drag-grabber').attr('id', `expression-${avatar}header`);
227227 $('#visual-novel-wrapper').append(template);
228228 dragElement($(template[0]));
229229 template.toggleClass('hidden', noSprites!spriteFile);
230230 await setImage(template.find('img'), defaultSpritePathspriteFile?.imageSrc || '');
231231 const fadeInPromise = new Promise(resolve => {
232232 template.fadeIn(250, () => resolve());
233233 });
234234 createCharacterPromisessetSpritePromises.push(fadeInPromise);
235- const setSpritePromise = setLastMessageSprite(template.find('img'), avatar, labels);
236- setSpritePromises.push(setSpritePromise);
237235 }
236+
237+ if (spriteFile) console.info(`Expression set for group member ${character.name}`, { expression: spriteFile.expression, file: spriteFile.fileName });
238+ else if (expressionImage.length) console.info(`Expression unset for group member ${character.name} - No sprite found`, { expression: expression });
239+ else console.info(`Expression not available for group member ${character.name}`, { expression: expression });
238240 }
239241
240- await Promise.allSettled(createCharacterPromises);
241242 return setSpritePromises;
242243}
243244
@@ -276,11 +277,11 @@ async function visualNovelUpdateLayers(container) {
276277 const containerWidth = container.width();
277278 const pivotalPoint = containerWidth * 0.5;
278279
279280 let images = Array.from($('#visual-novel-wrapper .expression-holder')).sort(sortFunction);
280281 let imagesWidth = [];
281282
282283 images.sort(sortFunction).eachforEach(functionimage ()=> {
283284 imagesWidth.push($(thisimage).width());
284285 });
285286
286287 let totalWidth = imagesWidth.reduce((a, b) => a + b, 0);
@@ -294,7 +295,7 @@ async function visualNovelUpdateLayers(container) {
294295 currentPosition = 0; // Reset the initial position to 0
295296 }
296297
297298 images.sort(sortFunction).eachforEach((indexcurrent, currentindex) => {
298299 const element = $(current);
299300 const elementID = element.attr('id');
300301
@@ -327,23 +328,6 @@ async function visualNovelUpdateLayers(container) {
327328 await Promise.allSettled(setLayerIndicesPromises);
328329}
329330
330-async function setLastMessageSprite(img, avatar, labels) {
331- const context = getContext();
332- const lastMessage = context.chat.slice().reverse().find(x => x.original_avatar == avatar || (x.force_avatar && x.force_avatar.includes(encodeURIComponent(avatar))));
333-
334- if (lastMessage) {
335- const text = lastMessage.mes || '';
336- const spriteFolderName = getSpriteFolderName(lastMessage, lastMessage.name);
337- const sprites = spriteCache[spriteFolderName] || [];
338- const label = await getExpressionLabel(text);
339- const path = labels.includes(label) ? sprites.find(x => x.label === label)?.path : '';
340-
341- if (path) {
342- setImage(img, path);
343- }
344- }
345-}
346-
347331async function setImage(img, path) {
348332 // Cohee: If something goes wrong, uncomment this to return to the old behavior
349333 /*
@@ -1062,28 +1046,28 @@ function removeExpression() {
10621046
10631047/**
10641048 * Validate a character's sprites, and redraw the sprites list if not done before or forced to redraw.
10651049 * @param {string} characterspriteFolderName - The character sprite folder to validate
10661050 * @param {boolean} [forceRedrawCached=false] - Whether to force redrawing the sprites list even if it's already been drawn before
10671051 */
10681052async function validateImages(characterspriteFolderName, forceRedrawCached = false) {
10691053 if (!characterspriteFolderName) {
10701054 return;
10711055 }
10721056
10731057 const labels = await getExpressionsList();
10741058
10751059 if (spriteCache[characterspriteFolderName]) {
10761060 if (forceRedrawCached && $('#image_list').data('name') !== characterspriteFolderName) {
10771061 console.debug('force redrawing character sprites list');
10781062 await drawSpritesList(characterspriteFolderName, labels, spriteCache[characterspriteFolderName]);
10791063 }
10801064
10811065 return;
10821066 }
10831067
10841068 const sprites = await getSpritesList(characterspriteFolderName);
10851069 let validExpressions = await drawSpritesList(characterspriteFolderName, labels, sprites);
10861070 spriteCache[characterspriteFolderName] = validExpressions;
10871071}
10881072
10891073/**
@@ -1106,20 +1090,20 @@ function getExpressionImageData(sprite) {
11061090
11071091/**
11081092 * Populate the character expression list with sprites for the given character.
11091093 * @param {string} characterspriteFolderName - The name of the character to populate the list for
11101094 * @param {string[]} labels - An array of expression labels that are valid
11111095 * @param {Expression[]} sprites - An array of sprites
11121096 * @returns {Promise<Expression[]>} An array of valid expression labels
11131097 */
11141098async function drawSpritesList(characterspriteFolderName, labels, sprites) {
11151099 /** @type {Expression[]} */
11161100 let validExpressions = [];
11171101
11181102 $('#no_chat_expressions').hide();
11191103 $('#open_chat_expressions').show();
11201104 $('#image_list').empty();
11211105 $('#image_list').data('name', characterspriteFolderName);
11221106 $('#image_list_header_name').text(characterspriteFolderName);
11231107
11241108 if (!Array.isArray(labels)) {
11251109 return [];
@@ -1331,23 +1315,24 @@ export async function getExpressionsList() {
13311315}
13321316
13331317/**
1334- * Set the expression of a character.
1318+ * Selects a sprite from the given sprite folder for the given expression.
1335- * @param {string} character - The name of the character
1319+ *
1336- * @param {string} expression - The expression or sprite name to set
1320+ * If multiple sprites are allowed for the expression, it will randomly select one.
1337- * @param {Object} options - Optional parameters
1321+ * If the rerollIfSame option is enabled, it will only select a different sprite if the previous sprite was the same.
1338- * @param {boolean} [options.force=false] - Whether to force the expression change even if Visual Novel mode is on
1322+ * If the overrideSpriteFile option is set, it will look for the sprite with the given file name instead of randomly selecting one.
1339- * @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name.
1323+ *
13401324 * @returnsparam {Promise<void>string} AspriteFolderName promise- thatThe resolvesname whenof the expression has beensprite set.folder
1325+ * @param {string} expression - The expression to find the sprite for
1326+ * @param {object} [options] - Options to select the sprite
1327+ * @param {string} [options.prevExpressionSrc=null] - The source of the previous expression
1328+ * @param {string} [options.overrideSpriteFile=null] - The file name of the sprite to select
1329+ * @returns {ExpressionImage} - The selected sprite
13411330 */
13421331async function setExpressionchooseSpriteForExpression(characterspriteFolderName, expression, { forceprevExpressionSrc = falsenull, overrideSpriteFile = null } = {}) {
1343- await validateImages(character);
1332+ const sprite = (spriteCache[spriteFolderName] && spriteCache[spriteFolderName].find(x => x.label === expression));
1344- const img = $('img.expression');
1333+ if (!(sprite?.files.length > 0))
1345- const prevExpressionSrc = img.attr('src');
1334+ return null;
1346- const expressionClone = img.clone();
13471335
1348- /** @type {Expression} */
1349- const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
1350- if (sprite && sprite.files.length > 0) {
13511336 let spriteFile = sprite.files[0];
13521337
13531338 // If a specific sprite file should be set, we are looking it up here
@@ -1360,28 +1345,47 @@ async function setExpression(character, expression, { force = false, overrideSpr
13601345 else if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) {
13611346 let possibleFiles = sprite.files;
13621347 if (extension_settings.expressions.rerollIfSame) {
13631348 possibleFiles = possibleFiles.filter(x => !prevExpressionSrc || x.imageSrc !== prevExpressionSrc);
13641349 }
13651350 spriteFile = possibleFiles[Math.floor(Math.random() * possibleFiles.length)];
13661351 }
13671352
1353+ return spriteFile;
1354+
1355+}
1356+
1357+/**
1358+ * Set the expression of a character.
1359+ * @param {string} spriteFolderName - The name of the character (folder name - can also be a costume override)
1360+ * @param {string} expression - The expression or sprite name to set
1361+ * @param {Object} options - Optional parameters
1362+ * @param {boolean} [options.force=false] - Whether to force the expression change even if Visual Novel mode is on
1363+ * @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name.
1364+ * @returns {Promise<void>} A promise that resolves when the expression has been set.
1365+ */
1366+async function setExpression(spriteFolderName, expression, { force = false, overrideSpriteFile = null } = {}) {
1367+ await validateImages(spriteFolderName);
1368+ const img = $('img.expression');
1369+ const prevExpressionSrc = img.attr('src');
1370+ const expressionClone = img.clone();
1371+
1372+ const spriteFile = chooseSpriteForExpression(spriteFolderName, expression, { prevExpressionSrc: prevExpressionSrc, overrideSpriteFile: overrideSpriteFile });
1373+ if (spriteFile) {
13681374 if (force && isVisualNovelMode()) {
13691375 const context = getContext();
13701376 const group = context.groups.find(x => x.id === context.groupId);
13711377
1372- for (const member of group.members) {
1378+ // If it's a folder, make sure we find the group member based on the actual name
1373- const groupMember = context.characters.find(x => x.avatar === member);
1379+ const memberName = spriteFolderName.split('/')[0] ?? spriteFolderName;
1374-
1375- if (!groupMember) {
1376- continue;
1377- }
13781380
1379- if (groupMember.name == character) {
1381+ const groupMember = group.members
1380- await setImage($(`.expression-holder[data-avatar="${member}"] img`), spriteFile.imageSrc);
1382+ .map(member => context.characters.find(x => x.avatar === member))
1383+ .find(groupMember => groupMember && groupMember.name === memberName);
1384+ if (groupMember) {
1385+ await setImage($(`.expression-holder[data-avatar="${groupMember.avatar}"] img`), spriteFile.imageSrc);
13811386 return;
13821387 }
13831388 }
1384- }
13851389
13861390 //only swap expressions when necessary
13871391 if (prevExpressionSrc !== spriteFile.imageSrc
@@ -1456,7 +1460,7 @@ async function setExpression(character, expression, { force = false, overrideSpr
14561460 } else {
14571461 setNone();
14581462 }
14591463 console.debug('Expression unset - No sprite found', { expression: expression });
14601464 }
14611465
14621466 function setDefault() {
@@ -1969,7 +1973,7 @@ function migrateSettings() {
19691973 $(document).on('click', '.expression_list_item', onClickExpressionImage);
19701974 $(document).on('click', '.expression_list_upload', onClickExpressionUpload);
19711975 $(document).on('click', '.expression_list_delete', onClickExpressionDelete);
19721976 $(window).on('resize', () => updateVisualNovelModeDebounced());
19731977 $('#open_chat_expressions').hide();
19741978
19751979 await renderAdditionalExpressionSettings();