Fix visual novel mode for new expression thingy
| @@ -12,7 +12,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | ||
| 12 | 12 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 13 | 13 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| 14 | 14 | import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 15 | 15 | import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 16 | 16 | import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; |
| 17 | 17 | import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; |
| 18 | 18 | import { Popup, POPUP_RESULT } from '../../popup.js'; |
| @@ -128,20 +128,20 @@ async function forceUpdateVisualNovelMode() { | ||
| 128 | 128 | const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, debounce_timeout.quick); |
| 129 | 129 | |
| 130 | 130 | async function updateVisualNovelMode(name, expression) { |
| 131 | 131 | const containervnContainer = $('#visual-novel-wrapper'); |
| 132 | 132 | |
| 133 | 133 | await visualNovelRemoveInactive(containervnContainer); |
| 134 | 134 | |
| 135 | 135 | const setSpritePromises = await visualNovelSetCharacterSprites(containervnContainer, name, expression); |
| 136 | 136 | |
| 137 | 137 | // calculate layer indices based on recent messages |
| 138 | 138 | await visualNovelUpdateLayers(containervnContainer); |
| 139 | 139 | |
| 140 | 140 | await Promise.allSettled(setSpritePromises); |
| 141 | 141 | |
| 142 | 142 | // update again based on new sprites |
| 143 | 143 | if (setSpritePromises.length > 0) { |
| 144 | 144 | await visualNovelUpdateLayers(containervnContainer); |
| 145 | 145 | } |
| 146 | 146 | } |
| 147 | 147 | |
| @@ -172,53 +172,53 @@ async function visualNovelRemoveInactive(container) { | ||
| 172 | 172 | await Promise.allSettled(removeInactiveCharactersPromises); |
| 173 | 173 | } |
| 174 | 174 | |
| 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) { | |
| 176 | 184 | const context = getContext(); |
| 177 | 185 | const group = context.groups.find(x => x.id == context.groupId); |
| 178 | - const labels = await getExpressionsList(); | |
| 179 | 186 | |
| 180 | - const createCharacterPromises = []; | |
| 181 | 187 | const setSpritePromises = []; |
| 182 | 188 | |
| 183 | 189 | for (const avatar of group.members) { |
| 184 | - const isDisabled = group.disabled_members.includes(avatar); | |
| 185 | - | |
| 186 | 190 | // skip disabled characters |
| 191 | + const isDisabled = group.disabled_members.includes(avatar); | |
| 187 | 192 | if (isDisabled && hideMutedSprites) { |
| 188 | 193 | continue; |
| 189 | 194 | } |
| 190 | 195 | |
| 191 | 196 | const character = context.characters.find(x => x.avatar == avatar); |
| 192 | - | |
| 193 | 197 | if (!character) { |
| 194 | 198 | continue; |
| 195 | 199 | } |
| 196 | 200 | |
| 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); | |
| 198 | 204 | |
| 199 | 205 | // download images if not downloaded yet |
| 200 | 206 | if (spriteCache[spriteFolderNamememberSpriteFolderName] === undefined) { |
| 201 | 207 | spriteCache[spriteFolderNamememberSpriteFolderName] = await getSpritesList(spriteFolderNamememberSpriteFolderName); |
| 202 | 208 | } |
| 203 | 209 | |
| 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; | |
| 210 | 211 | |
| 211 | - if (expressionImage.length > 0) { | |
| 212 | + const spriteFile = chooseSpriteForExpression(memberSpriteFolderName, expression, { prevExpressionSrc: prevExpressionSrc }); | |
| 212 | 213 | if (name == spriteFolderNameexpressionImage.length) { |
| 213 | - await validateImages(spriteFolderName, true); | |
| 214 | + if (spriteFolderName == memberSpriteFolderName) { | |
| 215 | + await validateImages(memberSpriteFolderName, true); | |
| 214 | 216 | 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 | 218 | const img = expressionImage.find('img'); |
| 219 | 219 | await setImage(img, path); |
| 220 | 220 | } |
| 221 | 221 | expressionImage.toggleClass('hidden', noSprites!spriteFile); |
| 222 | 222 | } else { |
| 223 | 223 | const template = $('#expression-holder').clone(); |
| 224 | 224 | template.attr('id', `expression-${avatar}`); |
| @@ -226,18 +226,19 @@ async function visualNovelSetCharacterSprites(container, name, expression) { | ||
| 226 | 226 | template.find('.drag-grabber').attr('id', `expression-${avatar}header`); |
| 227 | 227 | $('#visual-novel-wrapper').append(template); |
| 228 | 228 | dragElement($(template[0])); |
| 229 | 229 | template.toggleClass('hidden', noSprites!spriteFile); |
| 230 | 230 | await setImage(template.find('img'), defaultSpritePathspriteFile?.imageSrc || ''); |
| 231 | 231 | const fadeInPromise = new Promise(resolve => { |
| 232 | 232 | template.fadeIn(250, () => resolve()); |
| 233 | 233 | }); |
| 234 | 234 | createCharacterPromisessetSpritePromises.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 | } |
| 239 | 241 | |
| 240 | - await Promise.allSettled(createCharacterPromises); | |
| 241 | 242 | return setSpritePromises; |
| 242 | 243 | } |
| 243 | 244 | |
| @@ -276,11 +277,11 @@ async function visualNovelUpdateLayers(container) { | ||
| 276 | 277 | const containerWidth = container.width(); |
| 277 | 278 | const pivotalPoint = containerWidth * 0.5; |
| 278 | 279 | |
| 279 | 280 | let images = Array.from($('#visual-novel-wrapper .expression-holder')).sort(sortFunction); |
| 280 | 281 | let imagesWidth = []; |
| 281 | 282 | |
| 282 | 283 | images.sort(sortFunction).eachforEach(functionimage ()=> { |
| 283 | 284 | imagesWidth.push($(thisimage).width()); |
| 284 | 285 | }); |
| 285 | 286 | |
| 286 | 287 | let totalWidth = imagesWidth.reduce((a, b) => a + b, 0); |
| @@ -294,7 +295,7 @@ async function visualNovelUpdateLayers(container) { | ||
| 294 | 295 | currentPosition = 0; // Reset the initial position to 0 |
| 295 | 296 | } |
| 296 | 297 | |
| 297 | 298 | images.sort(sortFunction).eachforEach((indexcurrent, currentindex) => { |
| 298 | 299 | const element = $(current); |
| 299 | 300 | const elementID = element.attr('id'); |
| 300 | 301 | |
| @@ -327,23 +328,6 @@ async function visualNovelUpdateLayers(container) { | ||
| 327 | 328 | await Promise.allSettled(setLayerIndicesPromises); |
| 328 | 329 | } |
| 329 | 330 | |
| 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 | - | |
| 347 | 331 | async function setImage(img, path) { |
| 348 | 332 | // Cohee: If something goes wrong, uncomment this to return to the old behavior |
| 349 | 333 | /* |
| @@ -1062,28 +1046,28 @@ function removeExpression() { | ||
| 1062 | 1046 | |
| 1063 | 1047 | /** |
| 1064 | 1048 | * Validate a character's sprites, and redraw the sprites list if not done before or forced to redraw. |
| 1065 | 1049 | * @param {string} characterspriteFolderName - The character sprite folder to validate |
| 1066 | 1050 | * @param {boolean} [forceRedrawCached=false] - Whether to force redrawing the sprites list even if it's already been drawn before |
| 1067 | 1051 | */ |
| 1068 | 1052 | async function validateImages(characterspriteFolderName, forceRedrawCached = false) { |
| 1069 | 1053 | if (!characterspriteFolderName) { |
| 1070 | 1054 | return; |
| 1071 | 1055 | } |
| 1072 | 1056 | |
| 1073 | 1057 | const labels = await getExpressionsList(); |
| 1074 | 1058 | |
| 1075 | 1059 | if (spriteCache[characterspriteFolderName]) { |
| 1076 | 1060 | if (forceRedrawCached && $('#image_list').data('name') !== characterspriteFolderName) { |
| 1077 | 1061 | console.debug('force redrawing character sprites list'); |
| 1078 | 1062 | await drawSpritesList(characterspriteFolderName, labels, spriteCache[characterspriteFolderName]); |
| 1079 | 1063 | } |
| 1080 | 1064 | |
| 1081 | 1065 | return; |
| 1082 | 1066 | } |
| 1083 | 1067 | |
| 1084 | 1068 | const sprites = await getSpritesList(characterspriteFolderName); |
| 1085 | 1069 | let validExpressions = await drawSpritesList(characterspriteFolderName, labels, sprites); |
| 1086 | 1070 | spriteCache[characterspriteFolderName] = validExpressions; |
| 1087 | 1071 | } |
| 1088 | 1072 | |
| 1089 | 1073 | /** |
| @@ -1106,20 +1090,20 @@ function getExpressionImageData(sprite) { | ||
| 1106 | 1090 | |
| 1107 | 1091 | /** |
| 1108 | 1092 | * Populate the character expression list with sprites for the given character. |
| 1109 | 1093 | * @param {string} characterspriteFolderName - The name of the character to populate the list for |
| 1110 | 1094 | * @param {string[]} labels - An array of expression labels that are valid |
| 1111 | 1095 | * @param {Expression[]} sprites - An array of sprites |
| 1112 | 1096 | * @returns {Promise<Expression[]>} An array of valid expression labels |
| 1113 | 1097 | */ |
| 1114 | 1098 | async function drawSpritesList(characterspriteFolderName, labels, sprites) { |
| 1115 | 1099 | /** @type {Expression[]} */ |
| 1116 | 1100 | let validExpressions = []; |
| 1117 | 1101 | |
| 1118 | 1102 | $('#no_chat_expressions').hide(); |
| 1119 | 1103 | $('#open_chat_expressions').show(); |
| 1120 | 1104 | $('#image_list').empty(); |
| 1121 | 1105 | $('#image_list').data('name', characterspriteFolderName); |
| 1122 | 1106 | $('#image_list_header_name').text(characterspriteFolderName); |
| 1123 | 1107 | |
| 1124 | 1108 | if (!Array.isArray(labels)) { |
| 1125 | 1109 | return []; |
| @@ -1331,23 +1315,24 @@ export async function getExpressionsList() { | ||
| 1331 | 1315 | } |
| 1332 | 1316 | |
| 1333 | 1317 | /** |
| 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 | + * | |
| 1340 | 1324 | * @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 | |
| 1341 | 1330 | */ |
| 1342 | 1331 | async 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(); | |
| 1347 | 1335 | |
| 1348 | - /** @type {Expression} */ | |
| 1349 | - const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression)); | |
| 1350 | - if (sprite && sprite.files.length > 0) { | |
| 1351 | 1336 | let spriteFile = sprite.files[0]; |
| 1352 | 1337 | |
| 1353 | 1338 | // 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 | ||
| 1360 | 1345 | else if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) { |
| 1361 | 1346 | let possibleFiles = sprite.files; |
| 1362 | 1347 | if (extension_settings.expressions.rerollIfSame) { |
| 1363 | 1348 | possibleFiles = possibleFiles.filter(x => !prevExpressionSrc || x.imageSrc !== prevExpressionSrc); |
| 1364 | 1349 | } |
| 1365 | 1350 | spriteFile = possibleFiles[Math.floor(Math.random() * possibleFiles.length)]; |
| 1366 | 1351 | } |
| 1367 | 1352 | |
| 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) { | |
| 1368 | 1374 | if (force && isVisualNovelMode()) { |
| 1369 | 1375 | const context = getContext(); |
| 1370 | 1376 | const group = context.groups.find(x => x.id === context.groupId); |
| 1371 | 1377 | |
| 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 | - } | |
| 1378 | 1380 | |
| 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); | |
| 1381 | 1386 | return; |
| 1382 | 1387 | } |
| 1383 | 1388 | } |
| 1384 | - } | |
| 1385 | 1389 | |
| 1386 | 1390 | //only swap expressions when necessary |
| 1387 | 1391 | if (prevExpressionSrc !== spriteFile.imageSrc |
| @@ -1456,7 +1460,7 @@ async function setExpression(character, expression, { force = false, overrideSpr | ||
| 1456 | 1460 | } else { |
| 1457 | 1461 | setNone(); |
| 1458 | 1462 | } |
| 1459 | 1463 | console.debug('Expression unset - No sprite found', { expression: expression }); |
| 1460 | 1464 | } |
| 1461 | 1465 | |
| 1462 | 1466 | function setDefault() { |
| @@ -1969,7 +1973,7 @@ function migrateSettings() { | ||
| 1969 | 1973 | $(document).on('click', '.expression_list_item', onClickExpressionImage); |
| 1970 | 1974 | $(document).on('click', '.expression_list_upload', onClickExpressionUpload); |
| 1971 | 1975 | $(document).on('click', '.expression_list_delete', onClickExpressionDelete); |
| 1972 | 1976 | $(window).on('resize', () => updateVisualNovelModeDebounced()); |
| 1973 | 1977 | $('#open_chat_expressions').hide(); |
| 1974 | 1978 | |
| 1975 | 1979 | await renderAdditionalExpressionSettings(); |