Fix visual novel mode for new expression thingy

This commit is contained in:
Wolfsblvt
2025-02-01 04:46:30 +01:00
parent 198d10e759
commit 5c30244627

View File

@@ -12,7 +12,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js'; import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js'; import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js'; import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
import { Popup, POPUP_RESULT } from '../../popup.js'; import { Popup, POPUP_RESULT } from '../../popup.js';
@@ -128,20 +128,20 @@ async function forceUpdateVisualNovelMode() {
const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, debounce_timeout.quick); const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, debounce_timeout.quick);
async function updateVisualNovelMode(name, expression) { async function updateVisualNovelMode(name, expression) {
const container = $('#visual-novel-wrapper'); const vnContainer = $('#visual-novel-wrapper');
await visualNovelRemoveInactive(container); await visualNovelRemoveInactive(vnContainer);
const setSpritePromises = await visualNovelSetCharacterSprites(container, name, expression); const setSpritePromises = await visualNovelSetCharacterSprites(vnContainer, name, expression);
// calculate layer indices based on recent messages // calculate layer indices based on recent messages
await visualNovelUpdateLayers(container); await visualNovelUpdateLayers(vnContainer);
await Promise.allSettled(setSpritePromises); await Promise.allSettled(setSpritePromises);
// update again based on new sprites // update again based on new sprites
if (setSpritePromises.length > 0) { if (setSpritePromises.length > 0) {
await visualNovelUpdateLayers(container); await visualNovelUpdateLayers(vnContainer);
} }
} }
@@ -172,53 +172,53 @@ async function visualNovelRemoveInactive(container) {
await Promise.allSettled(removeInactiveCharactersPromises); await Promise.allSettled(removeInactiveCharactersPromises);
} }
async function visualNovelSetCharacterSprites(container, name, expression) { /**
* Sets the character sprites for visual novel mode based on the provided container, name, and expression.
*
* @param {JQuery<HTMLElement>} vnContainer - The container element where the sprites will be set
* @param {string} spriteFolderName - The name of the sprite folder
* @param {string} expression - The expression to set for the characters
* @returns {Promise<Array>} - An array of promises that resolve when the sprites are set
*/
async function visualNovelSetCharacterSprites(vnContainer, spriteFolderName, expression) {
const context = getContext(); const context = getContext();
const group = context.groups.find(x => x.id == context.groupId); const group = context.groups.find(x => x.id == context.groupId);
const labels = await getExpressionsList();
const createCharacterPromises = [];
const setSpritePromises = []; const setSpritePromises = [];
for (const avatar of group.members) { for (const avatar of group.members) {
const isDisabled = group.disabled_members.includes(avatar);
// skip disabled characters // skip disabled characters
const isDisabled = group.disabled_members.includes(avatar);
if (isDisabled && hideMutedSprites) { if (isDisabled && hideMutedSprites) {
continue; continue;
} }
const character = context.characters.find(x => x.avatar == avatar); const character = context.characters.find(x => x.avatar == avatar);
if (!character) { if (!character) {
continue; continue;
} }
const spriteFolderName = getSpriteFolderName({ original_avatar: character.avatar }, character.name); const expressionImage = vnContainer.find(`.expression-holder[data-avatar="${avatar}"]`);
const memberSpriteFolderName = getSpriteFolderName({ original_avatar: character.avatar }, character.name);
// download images if not downloaded yet // download images if not downloaded yet
if (spriteCache[spriteFolderName] === undefined) { if (spriteCache[memberSpriteFolderName] === undefined) {
spriteCache[spriteFolderName] = await getSpritesList(spriteFolderName); spriteCache[memberSpriteFolderName] = await getSpritesList(memberSpriteFolderName);
} }
const sprites = spriteCache[spriteFolderName]; const prevExpressionSrc = expressionImage.find('img').attr('src') || null;
const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`);
const defaultExpression = getFallbackExpression();
// TODO: Visual novel sprites need fixing, currently do not update based on multiple sprites, etc
const defaultSpritePath = sprites.find(x => x.label === defaultExpression)?.path;
const noSprites = sprites.length === 0;
if (expressionImage.length > 0) { const spriteFile = chooseSpriteForExpression(memberSpriteFolderName, expression, { prevExpressionSrc: prevExpressionSrc });
if (name == spriteFolderName) { if (expressionImage.length) {
await validateImages(spriteFolderName, true); if (spriteFolderName == memberSpriteFolderName) {
await validateImages(memberSpriteFolderName, true);
setExpressionOverrideHtml(true); // <= force clear expression override input setExpressionOverrideHtml(true); // <= force clear expression override input
const currentSpritePath = labels.includes(expression) ? sprites.find(x => x.label === expression)?.path : ''; const path = spriteFile?.imageSrc || '';
const path = currentSpritePath || defaultSpritePath || '';
const img = expressionImage.find('img'); const img = expressionImage.find('img');
await setImage(img, path); await setImage(img, path);
} }
expressionImage.toggleClass('hidden', noSprites); expressionImage.toggleClass('hidden', !spriteFile);
} else { } else {
const template = $('#expression-holder').clone(); const template = $('#expression-holder').clone();
template.attr('id', `expression-${avatar}`); template.attr('id', `expression-${avatar}`);
@@ -226,18 +226,19 @@ async function visualNovelSetCharacterSprites(container, name, expression) {
template.find('.drag-grabber').attr('id', `expression-${avatar}header`); template.find('.drag-grabber').attr('id', `expression-${avatar}header`);
$('#visual-novel-wrapper').append(template); $('#visual-novel-wrapper').append(template);
dragElement($(template[0])); dragElement($(template[0]));
template.toggleClass('hidden', noSprites); template.toggleClass('hidden', !spriteFile);
await setImage(template.find('img'), defaultSpritePath || ''); await setImage(template.find('img'), spriteFile?.imageSrc || '');
const fadeInPromise = new Promise(resolve => { const fadeInPromise = new Promise(resolve => {
template.fadeIn(250, () => resolve()); template.fadeIn(250, () => resolve());
}); });
createCharacterPromises.push(fadeInPromise); setSpritePromises.push(fadeInPromise);
const setSpritePromise = setLastMessageSprite(template.find('img'), avatar, labels); }
setSpritePromises.push(setSpritePromise);
} if (spriteFile) console.info(`Expression set for group member ${character.name}`, { expression: spriteFile.expression, file: spriteFile.fileName });
else if (expressionImage.length) console.info(`Expression unset for group member ${character.name} - No sprite found`, { expression: expression });
else console.info(`Expression not available for group member ${character.name}`, { expression: expression });
} }
await Promise.allSettled(createCharacterPromises);
return setSpritePromises; return setSpritePromises;
} }
@@ -276,11 +277,11 @@ async function visualNovelUpdateLayers(container) {
const containerWidth = container.width(); const containerWidth = container.width();
const pivotalPoint = containerWidth * 0.5; const pivotalPoint = containerWidth * 0.5;
let images = $('#visual-novel-wrapper .expression-holder'); let images = Array.from($('#visual-novel-wrapper .expression-holder')).sort(sortFunction);
let imagesWidth = []; let imagesWidth = [];
images.sort(sortFunction).each(function () { images.forEach(image => {
imagesWidth.push($(this).width()); imagesWidth.push($(image).width());
}); });
let totalWidth = imagesWidth.reduce((a, b) => a + b, 0); let totalWidth = imagesWidth.reduce((a, b) => a + b, 0);
@@ -294,7 +295,7 @@ async function visualNovelUpdateLayers(container) {
currentPosition = 0; // Reset the initial position to 0 currentPosition = 0; // Reset the initial position to 0
} }
images.sort(sortFunction).each((index, current) => { images.forEach((current, index) => {
const element = $(current); const element = $(current);
const elementID = element.attr('id'); const elementID = element.attr('id');
@@ -327,23 +328,6 @@ async function visualNovelUpdateLayers(container) {
await Promise.allSettled(setLayerIndicesPromises); await Promise.allSettled(setLayerIndicesPromises);
} }
async function setLastMessageSprite(img, avatar, labels) {
const context = getContext();
const lastMessage = context.chat.slice().reverse().find(x => x.original_avatar == avatar || (x.force_avatar && x.force_avatar.includes(encodeURIComponent(avatar))));
if (lastMessage) {
const text = lastMessage.mes || '';
const spriteFolderName = getSpriteFolderName(lastMessage, lastMessage.name);
const sprites = spriteCache[spriteFolderName] || [];
const label = await getExpressionLabel(text);
const path = labels.includes(label) ? sprites.find(x => x.label === label)?.path : '';
if (path) {
setImage(img, path);
}
}
}
async function setImage(img, path) { async function setImage(img, path) {
// Cohee: If something goes wrong, uncomment this to return to the old behavior // Cohee: If something goes wrong, uncomment this to return to the old behavior
/* /*
@@ -1062,28 +1046,28 @@ function removeExpression() {
/** /**
* Validate a character's sprites, and redraw the sprites list if not done before or forced to redraw. * Validate a character's sprites, and redraw the sprites list if not done before or forced to redraw.
* @param {string} character - The character to validate * @param {string} spriteFolderName - The character sprite folder to validate
* @param {boolean} [forceRedrawCached=false] - Whether to force redrawing the sprites list even if it's already been drawn before * @param {boolean} [forceRedrawCached=false] - Whether to force redrawing the sprites list even if it's already been drawn before
*/ */
async function validateImages(character, forceRedrawCached = false) { async function validateImages(spriteFolderName, forceRedrawCached = false) {
if (!character) { if (!spriteFolderName) {
return; return;
} }
const labels = await getExpressionsList(); const labels = await getExpressionsList();
if (spriteCache[character]) { if (spriteCache[spriteFolderName]) {
if (forceRedrawCached && $('#image_list').data('name') !== character) { if (forceRedrawCached && $('#image_list').data('name') !== spriteFolderName) {
console.debug('force redrawing character sprites list'); console.debug('force redrawing character sprites list');
await drawSpritesList(character, labels, spriteCache[character]); await drawSpritesList(spriteFolderName, labels, spriteCache[spriteFolderName]);
} }
return; return;
} }
const sprites = await getSpritesList(character); const sprites = await getSpritesList(spriteFolderName);
let validExpressions = await drawSpritesList(character, labels, sprites); let validExpressions = await drawSpritesList(spriteFolderName, labels, sprites);
spriteCache[character] = validExpressions; spriteCache[spriteFolderName] = validExpressions;
} }
/** /**
@@ -1106,20 +1090,20 @@ function getExpressionImageData(sprite) {
/** /**
* Populate the character expression list with sprites for the given character. * Populate the character expression list with sprites for the given character.
* @param {string} character - The name of the character to populate the list for * @param {string} spriteFolderName - The name of the character to populate the list for
* @param {string[]} labels - An array of expression labels that are valid * @param {string[]} labels - An array of expression labels that are valid
* @param {Expression[]} sprites - An array of sprites * @param {Expression[]} sprites - An array of sprites
* @returns {Promise<Expression[]>} An array of valid expression labels * @returns {Promise<Expression[]>} An array of valid expression labels
*/ */
async function drawSpritesList(character, labels, sprites) { async function drawSpritesList(spriteFolderName, labels, sprites) {
/** @type {Expression[]} */ /** @type {Expression[]} */
let validExpressions = []; let validExpressions = [];
$('#no_chat_expressions').hide(); $('#no_chat_expressions').hide();
$('#open_chat_expressions').show(); $('#open_chat_expressions').show();
$('#image_list').empty(); $('#image_list').empty();
$('#image_list').data('name', character); $('#image_list').data('name', spriteFolderName);
$('#image_list_header_name').text(character); $('#image_list_header_name').text(spriteFolderName);
if (!Array.isArray(labels)) { if (!Array.isArray(labels)) {
return []; return [];
@@ -1331,23 +1315,24 @@ export async function getExpressionsList() {
} }
/** /**
* Set the expression of a character. * Selects a sprite from the given sprite folder for the given expression.
* @param {string} character - The name of the character *
* @param {string} expression - The expression or sprite name to set * If multiple sprites are allowed for the expression, it will randomly select one.
* @param {Object} options - Optional parameters * If the rerollIfSame option is enabled, it will only select a different sprite if the previous sprite was the same.
* @param {boolean} [options.force=false] - Whether to force the expression change even if Visual Novel mode is on * If the overrideSpriteFile option is set, it will look for the sprite with the given file name instead of randomly selecting one.
* @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name. *
* @returns {Promise<void>} A promise that resolves when the expression has been set. * @param {string} spriteFolderName - The name of the sprite folder
* @param {string} expression - The expression to find the sprite for
* @param {object} [options] - Options to select the sprite
* @param {string} [options.prevExpressionSrc=null] - The source of the previous expression
* @param {string} [options.overrideSpriteFile=null] - The file name of the sprite to select
* @returns {ExpressionImage} - The selected sprite
*/ */
async function setExpression(character, expression, { force = false, overrideSpriteFile = null } = {}) { function chooseSpriteForExpression(spriteFolderName, expression, { prevExpressionSrc = null, overrideSpriteFile = null } = {}) {
await validateImages(character); const sprite = (spriteCache[spriteFolderName] && spriteCache[spriteFolderName].find(x => x.label === expression));
const img = $('img.expression'); if (!(sprite?.files.length > 0))
const prevExpressionSrc = img.attr('src'); return null;
const expressionClone = img.clone();
/** @type {Expression} */
const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
if (sprite && sprite.files.length > 0) {
let spriteFile = sprite.files[0]; let spriteFile = sprite.files[0];
// If a specific sprite file should be set, we are looking it up here // 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
else if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) { else if (extension_settings.expressions.allowMultiple && sprite.files.length > 1) {
let possibleFiles = sprite.files; let possibleFiles = sprite.files;
if (extension_settings.expressions.rerollIfSame) { if (extension_settings.expressions.rerollIfSame) {
possibleFiles = possibleFiles.filter(x => x.imageSrc !== prevExpressionSrc); possibleFiles = possibleFiles.filter(x => !prevExpressionSrc || x.imageSrc !== prevExpressionSrc);
} }
spriteFile = possibleFiles[Math.floor(Math.random() * possibleFiles.length)]; spriteFile = possibleFiles[Math.floor(Math.random() * possibleFiles.length)];
} }
return spriteFile;
}
/**
* Set the expression of a character.
* @param {string} spriteFolderName - The name of the character (folder name - can also be a costume override)
* @param {string} expression - The expression or sprite name to set
* @param {Object} options - Optional parameters
* @param {boolean} [options.force=false] - Whether to force the expression change even if Visual Novel mode is on
* @param {string?} [options.overrideSpriteFile=null] - Set if a specific sprite file should be used. Must be sprite file name.
* @returns {Promise<void>} A promise that resolves when the expression has been set.
*/
async function setExpression(spriteFolderName, expression, { force = false, overrideSpriteFile = null } = {}) {
await validateImages(spriteFolderName);
const img = $('img.expression');
const prevExpressionSrc = img.attr('src');
const expressionClone = img.clone();
const spriteFile = chooseSpriteForExpression(spriteFolderName, expression, { prevExpressionSrc: prevExpressionSrc, overrideSpriteFile: overrideSpriteFile });
if (spriteFile) {
if (force && isVisualNovelMode()) { if (force && isVisualNovelMode()) {
const context = getContext(); const context = getContext();
const group = context.groups.find(x => x.id === context.groupId); const group = context.groups.find(x => x.id === context.groupId);
for (const member of group.members) { // If it's a folder, make sure we find the group member based on the actual name
const groupMember = context.characters.find(x => x.avatar === member); const memberName = spriteFolderName.split('/')[0] ?? spriteFolderName;
if (!groupMember) { const groupMember = group.members
continue; .map(member => context.characters.find(x => x.avatar === member))
} .find(groupMember => groupMember && groupMember.name === memberName);
if (groupMember) {
if (groupMember.name == character) { await setImage($(`.expression-holder[data-avatar="${groupMember.avatar}"] img`), spriteFile.imageSrc);
await setImage($(`.expression-holder[data-avatar="${member}"] img`), spriteFile.imageSrc);
return; return;
} }
} }
}
//only swap expressions when necessary //only swap expressions when necessary
if (prevExpressionSrc !== spriteFile.imageSrc if (prevExpressionSrc !== spriteFile.imageSrc
@@ -1456,7 +1460,7 @@ async function setExpression(character, expression, { force = false, overrideSpr
} else { } else {
setNone(); setNone();
} }
console.debug('Expression unset'); console.debug('Expression unset - No sprite found', { expression: expression });
} }
function setDefault() { function setDefault() {
@@ -1969,7 +1973,7 @@ function migrateSettings() {
$(document).on('click', '.expression_list_item', onClickExpressionImage); $(document).on('click', '.expression_list_item', onClickExpressionImage);
$(document).on('click', '.expression_list_upload', onClickExpressionUpload); $(document).on('click', '.expression_list_upload', onClickExpressionUpload);
$(document).on('click', '.expression_list_delete', onClickExpressionDelete); $(document).on('click', '.expression_list_delete', onClickExpressionDelete);
$(window).on('resize', updateVisualNovelModeDebounced); $(window).on('resize', () => updateVisualNovelModeDebounced());
$('#open_chat_expressions').hide(); $('#open_chat_expressions').hide();
await renderAdditionalExpressionSettings(); await renderAdditionalExpressionSettings();