Fix visual novel mode for new expression thingy

5c30244627bd2184f9e126c1fc205e2e020ab0b3

Wolfsblvt <wolfsblvt@gmail.com>

1 files changed, +107 -103Ignore whitespace
public/scripts/extensions/expressions/index.js+107 -103
@@ -12,7 +12,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
12import { SlashCommand } from '../../slash-commands/SlashCommand.js';12import { SlashCommand } from '../../slash-commands/SlashCommand.js';
13import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';13import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
14import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';14import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
15import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';15import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
16import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';16import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
17import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';17import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
18import { Popup, POPUP_RESULT } from '../../popup.js';18import { Popup, POPUP_RESULT } from '../../popup.js';
@@ -128,20 +128,20 @@ async function forceUpdateVisualNovelMode() {
128const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, debounce_timeout.quick);128const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, debounce_timeout.quick);
129129
130async function updateVisualNovelMode(name, expression) {130async function updateVisualNovelMode(name, expression) {
131 const container = $('#visual-novel-wrapper');131 const vnContainer = $('#visual-novel-wrapper');
132132
133 await visualNovelRemoveInactive(container);133 await visualNovelRemoveInactive(vnContainer);
134134
135 const setSpritePromises = await visualNovelSetCharacterSprites(container, name, expression);135 const setSpritePromises = await visualNovelSetCharacterSprites(vnContainer, name, expression);
136136
137 // calculate layer indices based on recent messages137 // calculate layer indices based on recent messages
138 await visualNovelUpdateLayers(container);138 await visualNovelUpdateLayers(vnContainer);
139139
140 await Promise.allSettled(setSpritePromises);140 await Promise.allSettled(setSpritePromises);
141141
142 // update again based on new sprites142 // update again based on new sprites
143 if (setSpritePromises.length > 0) {143 if (setSpritePromises.length > 0) {
144 await visualNovelUpdateLayers(container);144 await visualNovelUpdateLayers(vnContainer);
145 }145 }
146}146}
147147
@@ -172,53 +172,53 @@ async function visualNovelRemoveInactive(container) {
172 await Promise.allSettled(removeInactiveCharactersPromises);172 await Promise.allSettled(removeInactiveCharactersPromises);
173}173}
174174
175async 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 */
183async function visualNovelSetCharacterSprites(vnContainer, spriteFolderName, expression) {
176 const context = getContext();184 const context = getContext();
177 const group = context.groups.find(x => x.id == context.groupId);185 const group = context.groups.find(x => x.id == context.groupId);
178 const labels = await getExpressionsList();
179186
180 const createCharacterPromises = [];
181 const setSpritePromises = [];187 const setSpritePromises = [];
182188
183 for (const avatar of group.members) {189 for (const avatar of group.members) {
184 const isDisabled = group.disabled_members.includes(avatar);
185
186 // skip disabled characters190 // skip disabled characters
191 const isDisabled = group.disabled_members.includes(avatar);
187 if (isDisabled && hideMutedSprites) {192 if (isDisabled && hideMutedSprites) {
188 continue;193 continue;
189 }194 }
190195
191 const character = context.characters.find(x => x.avatar == avatar);196 const character = context.characters.find(x => x.avatar == avatar);
192
193 if (!character) {197 if (!character) {
194 continue;198 continue;
195 }199 }
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
199 // download images if not downloaded yet205 // download images if not downloaded yet
200 if (spriteCache[spriteFolderName] === undefined) {206 if (spriteCache[memberSpriteFolderName] === undefined) {
201 spriteCache[spriteFolderName] = await getSpritesList(spriteFolderName);207 spriteCache[memberSpriteFolderName] = await getSpritesList(memberSpriteFolderName);
202 }208 }
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 });
212 if (name == spriteFolderName) {213 if (expressionImage.length) {
213 await validateImages(spriteFolderName, true);214 if (spriteFolderName == memberSpriteFolderName) {
215 await validateImages(memberSpriteFolderName, true);
214 setExpressionOverrideHtml(true); // <= force clear expression override input216 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 || '';
218 const img = expressionImage.find('img');218 const img = expressionImage.find('img');
219 await setImage(img, path);219 await setImage(img, path);
220 }220 }
221 expressionImage.toggleClass('hidden', noSprites);221 expressionImage.toggleClass('hidden', !spriteFile);
222 } else {222 } else {
223 const template = $('#expression-holder').clone();223 const template = $('#expression-holder').clone();
224 template.attr('id', `expression-${avatar}`);224 template.attr('id', `expression-${avatar}`);
@@ -226,18 +226,19 @@ async function visualNovelSetCharacterSprites(container, name, expression) {
226 template.find('.drag-grabber').attr('id', `expression-${avatar}header`);226 template.find('.drag-grabber').attr('id', `expression-${avatar}header`);
227 $('#visual-novel-wrapper').append(template);227 $('#visual-novel-wrapper').append(template);
228 dragElement($(template[0]));228 dragElement($(template[0]));
229 template.toggleClass('hidden', noSprites);229 template.toggleClass('hidden', !spriteFile);
230 await setImage(template.find('img'), defaultSpritePath || '');230 await setImage(template.find('img'), spriteFile?.imageSrc || '');
231 const fadeInPromise = new Promise(resolve => {231 const fadeInPromise = new Promise(resolve => {
232 template.fadeIn(250, () => resolve());232 template.fadeIn(250, () => resolve());
233 });233 });
234 createCharacterPromises.push(fadeInPromise);234 setSpritePromises.push(fadeInPromise);
235 const setSpritePromise = setLastMessageSprite(template.find('img'), avatar, labels);
236 setSpritePromises.push(setSpritePromise);
237 }235 }
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 });
238 }240 }
239241
240 await Promise.allSettled(createCharacterPromises);
241 return setSpritePromises;242 return setSpritePromises;
242}243}
243244
@@ -276,11 +277,11 @@ async function visualNovelUpdateLayers(container) {
276 const containerWidth = container.width();277 const containerWidth = container.width();
277 const pivotalPoint = containerWidth * 0.5;278 const pivotalPoint = containerWidth * 0.5;
278279
279 let images = $('#visual-novel-wrapper .expression-holder');280 let images = Array.from($('#visual-novel-wrapper .expression-holder')).sort(sortFunction);
280 let imagesWidth = [];281 let imagesWidth = [];
281282
282 images.sort(sortFunction).each(function () {283 images.forEach(image => {
283 imagesWidth.push($(this).width());284 imagesWidth.push($(image).width());
284 });285 });
285286
286 let totalWidth = imagesWidth.reduce((a, b) => a + b, 0);287 let totalWidth = imagesWidth.reduce((a, b) => a + b, 0);
@@ -294,7 +295,7 @@ async function visualNovelUpdateLayers(container) {
294 currentPosition = 0; // Reset the initial position to 0295 currentPosition = 0; // Reset the initial position to 0
295 }296 }
296297
297 images.sort(sortFunction).each((index, current) => {298 images.forEach((current, index) => {
298 const element = $(current);299 const element = $(current);
299 const elementID = element.attr('id');300 const elementID = element.attr('id');
300301
@@ -327,23 +328,6 @@ async function visualNovelUpdateLayers(container) {
327 await Promise.allSettled(setLayerIndicesPromises);328 await Promise.allSettled(setLayerIndicesPromises);
328}329}
329330
330async 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
347async function setImage(img, path) {331async function setImage(img, path) {
348 // Cohee: If something goes wrong, uncomment this to return to the old behavior332 // Cohee: If something goes wrong, uncomment this to return to the old behavior
349 /*333 /*
@@ -1062,28 +1046,28 @@ function removeExpression() {
10621046
1063/**1047/**
1064 * Validate a character's sprites, and redraw the sprites list if not done before or forced to redraw.1048 * Validate a character's sprites, and redraw the sprites list if not done before or forced to redraw.
1065 * @param {string} character - The character to validate1049 * @param {string} spriteFolderName - The character sprite folder to validate
1066 * @param {boolean} [forceRedrawCached=false] - Whether to force redrawing the sprites list even if it's already been drawn before1050 * @param {boolean} [forceRedrawCached=false] - Whether to force redrawing the sprites list even if it's already been drawn before
1067 */1051 */
1068async function validateImages(character, forceRedrawCached = false) {1052async function validateImages(spriteFolderName, forceRedrawCached = false) {
1069 if (!character) {1053 if (!spriteFolderName) {
1070 return;1054 return;
1071 }1055 }
10721056
1073 const labels = await getExpressionsList();1057 const labels = await getExpressionsList();
10741058
1075 if (spriteCache[character]) {1059 if (spriteCache[spriteFolderName]) {
1076 if (forceRedrawCached && $('#image_list').data('name') !== character) {1060 if (forceRedrawCached && $('#image_list').data('name') !== spriteFolderName) {
1077 console.debug('force redrawing character sprites list');1061 console.debug('force redrawing character sprites list');
1078 await drawSpritesList(character, labels, spriteCache[character]);1062 await drawSpritesList(spriteFolderName, labels, spriteCache[spriteFolderName]);
1079 }1063 }
10801064
1081 return;1065 return;
1082 }1066 }
10831067
1084 const sprites = await getSpritesList(character);1068 const sprites = await getSpritesList(spriteFolderName);
1085 let validExpressions = await drawSpritesList(character, labels, sprites);1069 let validExpressions = await drawSpritesList(spriteFolderName, labels, sprites);
1086 spriteCache[character] = validExpressions;1070 spriteCache[spriteFolderName] = validExpressions;
1087}1071}
10881072
1089/**1073/**
@@ -1106,20 +1090,20 @@ function getExpressionImageData(sprite) {
11061090
1107/**1091/**
1108 * Populate the character expression list with sprites for the given character.1092 * Populate the character expression list with sprites for the given character.
1109 * @param {string} character - The name of the character to populate the list for1093 * @param {string} spriteFolderName - The name of the character to populate the list for
1110 * @param {string[]} labels - An array of expression labels that are valid1094 * @param {string[]} labels - An array of expression labels that are valid
1111 * @param {Expression[]} sprites - An array of sprites1095 * @param {Expression[]} sprites - An array of sprites
1112 * @returns {Promise<Expression[]>} An array of valid expression labels1096 * @returns {Promise<Expression[]>} An array of valid expression labels
1113 */1097 */
1114async function drawSpritesList(character, labels, sprites) {1098async function drawSpritesList(spriteFolderName, labels, sprites) {
1115 /** @type {Expression[]} */1099 /** @type {Expression[]} */
1116 let validExpressions = [];1100 let validExpressions = [];
11171101
1118 $('#no_chat_expressions').hide();1102 $('#no_chat_expressions').hide();
1119 $('#open_chat_expressions').show();1103 $('#open_chat_expressions').show();
1120 $('#image_list').empty();1104 $('#image_list').empty();
1121 $('#image_list').data('name', character);1105 $('#image_list').data('name', spriteFolderName);
1122 $('#image_list_header_name').text(character);1106 $('#image_list_header_name').text(spriteFolderName);
11231107
1124 if (!Array.isArray(labels)) {1108 if (!Array.isArray(labels)) {
1125 return [];1109 return [];
@@ -1331,55 +1315,75 @@ export async function getExpressionsList() {
1331}1315}
13321316
1333/**1317/**
1318 * Selects a sprite from the given sprite folder for the given expression.
1319 *
1320 * If multiple sprites are allowed for the expression, it will randomly select one.
1321 * If the rerollIfSame option is enabled, it will only select a different sprite if the previous sprite was the same.
1322 * If the overrideSpriteFile option is set, it will look for the sprite with the given file name instead of randomly selecting one.
1323 *
1324 * @param {string} spriteFolderName - The name of the sprite 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
1330 */
1331function chooseSpriteForExpression(spriteFolderName, expression, { prevExpressionSrc = null, overrideSpriteFile = null } = {}) {
1332 const sprite = (spriteCache[spriteFolderName] && spriteCache[spriteFolderName].find(x => x.label === expression));
1333 if (!(sprite?.files.length > 0))
1334 return null;
1335
1336 let spriteFile = sprite.files[0];
1337
1338 // If a specific sprite file should be set, we are looking it up here
1339 if (overrideSpriteFile) {
1340 const searched = sprite.files.find(x => x.fileName === overrideSpriteFile);
1341 if (searched) spriteFile = searched;
1342 else toastr.warning(t`Couldn't find sprite file ${overrideSpriteFile} for expression ${expression}.`, t`Sprite Not Found`);
1343 }
1344 // Else calculate next expression, if multiple are allowed
1345 else if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) {
1346 let possibleFiles = sprite.files;
1347 if (extension_settings.expressions.rerollIfSame) {
1348 possibleFiles = possibleFiles.filter(x => !prevExpressionSrc || x.imageSrc !== prevExpressionSrc);
1349 }
1350 spriteFile = possibleFiles[Math.floor(Math.random() * possibleFiles.length)];
1351 }
1352
1353 return spriteFile;
1354
1355}
1356
1357/**
1334 * Set the expression of a character.1358 * Set the expression of a character.
1335 * @param {string} character - The name of the character1359 * @param {string} spriteFolderName - The name of the character (folder name - can also be a costume override)
1336 * @param {string} expression - The expression or sprite name to set1360 * @param {string} expression - The expression or sprite name to set
1337 * @param {Object} options - Optional parameters1361 * @param {Object} options - Optional parameters
1338 * @param {boolean} [options.force=false] - Whether to force the expression change even if Visual Novel mode is on1362 * @param {boolean} [options.force=false] - Whether to force the expression change even if Visual Novel mode is on
1339 * @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name.1363 * @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name.
1340 * @returns {Promise<void>} A promise that resolves when the expression has been set.1364 * @returns {Promise<void>} A promise that resolves when the expression has been set.
1341 */1365 */
1342async function setExpression(character, expression, { force = false, overrideSpriteFile = null } = {}) {1366async function setExpression(spriteFolderName, expression, { force = false, overrideSpriteFile = null } = {}) {
1343 await validateImages(character);1367 await validateImages(spriteFolderName);
1344 const img = $('img.expression');1368 const img = $('img.expression');
1345 const prevExpressionSrc = img.attr('src');1369 const prevExpressionSrc = img.attr('src');
1346 const expressionClone = img.clone();1370 const expressionClone = img.clone();
13471371
1348 /** @type {Expression} */1372 const spriteFile = chooseSpriteForExpression(spriteFolderName, expression, { prevExpressionSrc: prevExpressionSrc, overrideSpriteFile: overrideSpriteFile });
1349 const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));1373 if (spriteFile) {
1350 if (sprite && sprite.files.length > 0) {
1351 let spriteFile = sprite.files[0];
1352
1353 // If a specific sprite file should be set, we are looking it up here
1354 if (overrideSpriteFile) {
1355 const searched = sprite.files.find(x => x.fileName === overrideSpriteFile);
1356 if (searched) spriteFile = searched;
1357 else toastr.warning(t`Couldn't find sprite file ${overrideSpriteFile} for expression ${expression}.`, t`Sprite Not Found`);
1358 }
1359 // Else calculate next expression, if multiple are allowed
1360 else if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) {
1361 let possibleFiles = sprite.files;
1362 if (extension_settings.expressions.rerollIfSame) {
1363 possibleFiles = possibleFiles.filter(x => x.imageSrc !== prevExpressionSrc);
1364 }
1365 spriteFile = possibleFiles[Math.floor(Math.random() * possibleFiles.length)];
1366 }
1367
1368 if (force && isVisualNovelMode()) {1374 if (force && isVisualNovelMode()) {
1369 const context = getContext();1375 const context = getContext();
1370 const group = context.groups.find(x => x.id === context.groupId);1376 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))
1381 return;1383 .find(groupMember => groupMember && groupMember.name === memberName);
1382 }1384 if (groupMember) {
1385 await setImage($(`.expression-holder[data-avatar="${groupMember.avatar}"] img`), spriteFile.imageSrc);
1386 return;
1383 }1387 }
1384 }1388 }
13851389
@@ -1456,7 +1460,7 @@ async function setExpression(character, expression, { force = false, overrideSpr
1456 } else {1460 } else {
1457 setNone();1461 setNone();
1458 }1462 }
1459 console.debug('Expression unset');1463 console.debug('Expression unset - No sprite found', { expression: expression });
1460 }1464 }
14611465
1462 function setDefault() {1466 function setDefault() {
@@ -1969,7 +1973,7 @@ function migrateSettings() {
1969 $(document).on('click', '.expression_list_item', onClickExpressionImage);1973 $(document).on('click', '.expression_list_item', onClickExpressionImage);
1970 $(document).on('click', '.expression_list_upload', onClickExpressionUpload);1974 $(document).on('click', '.expression_list_upload', onClickExpressionUpload);
1971 $(document).on('click', '.expression_list_delete', onClickExpressionDelete);1975 $(document).on('click', '.expression_list_delete', onClickExpressionDelete);
1972 $(window).on('resize', updateVisualNovelModeDebounced);1976 $(window).on('resize', () => updateVisualNovelModeDebounced());
1973 $('#open_chat_expressions').hide();1977 $('#open_chat_expressions').hide();
19741978
1975 await renderAdditionalExpressionSettings();1979 await renderAdditionalExpressionSettings();