Roll on the sprite to use for an expression

adede8b6befe1c6c21e8b6ae3b28c44e746580ec

Wolfsblvt <wolfsblvt@gmail.com>

2 files changed, +54 -17Ignore whitespace
public/scripts/extensions/expressions/index.js+53 -16
@@ -360,7 +360,7 @@ async function setImage(img, path) {
360360 return new Promise(resolve => {
361361 const prevExpressionSrc = img.attr('src');
362362 const expressionClone = img.clone();
363363 const originalId = img.attrdata('idfilename');
364364
365365 //only swap expressions when necessary
366366 if (prevExpressionSrc !== path && !img.hasClass('expression-animating')) {
@@ -368,7 +368,7 @@ async function setImage(img, path) {
368368 expressionClone.addClass('expression-clone');
369369 //make invisible and remove id to prevent double ids
370370 //must be made invisible to start because they share the same Z-index
371371 expressionClone.attrdata('idfilename', '').css({ opacity: 0 });
372372 //add new sprite path to clone src
373373 expressionClone.attr('src', path);
374374 //add invisible clone to html
@@ -404,7 +404,7 @@ async function setImage(img, path) {
404404 //remove old expression
405405 img.remove();
406406 //replace ID so it becomes the new 'original' expression for next change
407407 expressionClone.attrdata('idfilename', originalId);
408408 expressionClone.removeClass('expression-animating');
409409
410410 // Reset the expression holder min height and width
@@ -1311,9 +1311,9 @@ function removeExpression() {
13111311/**
13121312 * Validate a character's sprites, and redraw the sprites list if not done before or forced to redraw.
13131313 * @param {string} character - The character to validate
13141314 * @param {boolean} [forceRedrawCached=false] - Whether to force redrawing the sprites list even if it's already been drawn before
13151315 */
13161316async function validateImages(character, forceRedrawCached = false) {
13171317 if (!character) {
13181318 return;
13191319 }
@@ -1493,6 +1493,13 @@ async function renderFallbackExpressionPicker() {
14931493 }
14941494}
14951495
1496+/**
1497+ * Retrieves a unique list of cached expressions.
1498+ * Combines the default expressions list with custom user-defined expressions.
1499+ *
1500+ * @returns {string[]} An array of unique expression labels
1501+ */
1502+
14961503function getCachedExpressions() {
14971504 if (!Array.isArray(expressionsList)) {
14981505 return [];
@@ -1558,7 +1565,14 @@ export async function getExpressionsList() {
15581565 return [...result, ...extension_settings.expressions.custom].filter(onlyUnique);
15591566}
15601567
1561-async function setExpression(character, expression, force) {
1568+/**
1569+ * Set the expression of a character.
1570+ * @param {string} character - The name of the character
1571+ * @param {string} expression - The expression to set
1572+ * @param {boolean} [force=false] - Whether to force the expression change even if Visual Novel mode is on.
1573+ * @returns {Promise<void>} A promise that resolves when the expression has been set.
1574+ */
1575+async function setExpression(character, expression, force = false) {
15621576 if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {
15631577 console.debug('entered setExpressions');
15641578 await validateImages(character);
@@ -1566,11 +1580,23 @@ async function setExpression(character, expression, force) {
15661580 const prevExpressionSrc = img.attr('src');
15671581 const expressionClone = img.clone();
15681582
1583+ /** @type {Expression} */
15691584 const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
15701585 console.debug('checking for expression images to show..');
15711586 if (sprite) {
15721587 console.debug('setting expression from character images folder');
15731588
1589+ let spriteFile = sprite.files[0];
1590+
1591+ // Calculate next expression
1592+ if (sprite.files.length > 1) {
1593+ let possibleFiles = sprite.files;
1594+ if (extension_settings.expressions_reroll_if_same) {
1595+ possibleFiles = possibleFiles.filter(x => x.imageSrc !== prevExpressionSrc);
1596+ }
1597+ spriteFile = possibleFiles[Math.floor(Math.random() * possibleFiles.length)];
1598+ }
1599+
15741600 if (force && isVisualNovelMode()) {
15751601 const context = getContext();
15761602 const group = context.groups.find(x => x.id === context.groupId);
@@ -1583,13 +1609,13 @@ async function setExpression(character, expression, force) {
15831609 }
15841610
15851611 if (groupMember.name == character) {
15861612 await setImage($(`.expression-holder[data-avatar="${member}"] img`), spritespriteFile.pathimageSrc);
15871613 return;
15881614 }
15891615 }
15901616 }
15911617 //only swap expressions when necessary
15921618 if (prevExpressionSrc !== spritespriteFile.pathimageSrc
15931619 && !img.hasClass('expression-animating')) {
15941620 //clone expression
15951621 expressionClone.addClass('expression-clone');
@@ -1597,7 +1623,7 @@ async function setExpression(character, expression, force) {
15971623 //must be made invisible to start because they share the same Z-index
15981624 expressionClone.attr('id', '').css({ opacity: 0 });
15991625 //add new sprite path to clone src
16001626 expressionClone.attr('src', spritespriteFile.pathimageSrc);
16011627 //add invisible clone to html
16021628 expressionClone.appendTo($('#expression-holder'));
16031629
@@ -1645,7 +1671,7 @@ async function setExpression(character, expression, force) {
16451671 expressionClone.removeClass('default');
16461672 expressionClone.off('error');
16471673 expressionClone.on('error', function () {
16481674 console.debug('Expression image error', spritespriteFile.pathimageSrc);
16491675 $(this).attr('src', '');
16501676 $(this).off('error');
16511677 if (force && extension_settings.expressions.showDefault) {
@@ -1706,7 +1732,7 @@ async function setExpression(character, expression, force) {
17061732}
17071733
17081734function onClickExpressionImage() {
17091735 const expression = $(this).attrdata('idexpression');
17101736 setSpriteSlashCommand({}, expression);
17111737}
17121738
@@ -1830,7 +1856,7 @@ async function onClickExpressionUpload(event) {
18301856 // Prevents the expression from being set
18311857 event.stopPropagation();
18321858
18331859 const idexpression = $(this).closest('.expression_list_item').attrdata('idexpression');
18341860 const name = $('#image_list').data('name');
18351861
18361862 const handleExpressionUploadChange = async (e) => {
@@ -1840,9 +1866,20 @@ async function onClickExpressionUpload(event) {
18401866 return;
18411867 }
18421868
1869+ // // If extension_settings.expressions.allowMultiple is false and there's already a main image, ask user:
1870+ // let hasMainImage = true; // Check from your item data
1871+ // if (!extension_settings.expressions.allowMultiple && hasMainImage) {
1872+ // let userChoice = await callPopup('<h3>Replace existing main image?</h3><p>Press Ok to replace, Cancel to abort.</p>', 'confirm');
1873+ // if (!userChoice) {
1874+ // return;
1875+ // }
1876+ // // If user chooses replace, remove the old file, then proceed
1877+ // // ...existing code to remove old file...
1878+ // }
1879+
18431880 const formData = new FormData();
18441881 formData.append('name', name);
18451882 formData.append('label', idexpression);
18461883 formData.append('avatar', file);
18471884
18481885 await handleFileUpload('/api/sprites/upload', formData);
@@ -1851,7 +1888,7 @@ async function onClickExpressionUpload(event) {
18511888 e.target.form.reset();
18521889
18531890 // In Talkinghead mode, when a new talkinghead image is uploaded, refresh the live char.
18541891 if (idexpression === 'talkinghead' && isTalkingHeadEnabled() && modules.includes('talkinghead')) {
18551892 await loadTalkingHead();
18561893 }
18571894 };
@@ -1987,14 +2024,14 @@ async function onClickExpressionDelete(event) {
19872024 return;
19882025 }
19892026
19902027 const idexpression = $(this).closest('.expression_list_item').attrdata('idexpression');
19912028 const name = $('#image_list').data('name');
19922029
19932030 try {
19942031 await fetch('/api/sprites/delete', {
19952032 method: 'POST',
19962033 headers: getRequestHeaders(),
19972034 body: JSON.stringify({ name, label: idexpression }),
19982035 });
19992036 } catch (error) {
20002037 toastr.error('Failed to delete image. Try again later.');
public/scripts/extensions/expressions/list-item.html+1 -1
@@ -1,5 +1,5 @@
11{{#each images}}
22<div class="expression_list_item" data-epressionexpression="{{../expression}}" data-expression-type="{{this.type}}" data-filename="{{this.fileName}}">
33 <div class="expression_list_buttons">
44 <div class="menu_button expression_list_upload" title="Upload image">
55 <i class="fa-solid fa-upload"></i>