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) {
360 return new Promise(resolve => {360 return new Promise(resolve => {
361 const prevExpressionSrc = img.attr('src');361 const prevExpressionSrc = img.attr('src');
362 const expressionClone = img.clone();362 const expressionClone = img.clone();
363 const originalId = img.attr('id');363 const originalId = img.data('filename');
364364
365 //only swap expressions when necessary365 //only swap expressions when necessary
366 if (prevExpressionSrc !== path && !img.hasClass('expression-animating')) {366 if (prevExpressionSrc !== path && !img.hasClass('expression-animating')) {
@@ -368,7 +368,7 @@ async function setImage(img, path) {
368 expressionClone.addClass('expression-clone');368 expressionClone.addClass('expression-clone');
369 //make invisible and remove id to prevent double ids369 //make invisible and remove id to prevent double ids
370 //must be made invisible to start because they share the same Z-index370 //must be made invisible to start because they share the same Z-index
371 expressionClone.attr('id', '').css({ opacity: 0 });371 expressionClone.data('filename', '').css({ opacity: 0 });
372 //add new sprite path to clone src372 //add new sprite path to clone src
373 expressionClone.attr('src', path);373 expressionClone.attr('src', path);
374 //add invisible clone to html374 //add invisible clone to html
@@ -404,7 +404,7 @@ async function setImage(img, path) {
404 //remove old expression404 //remove old expression
405 img.remove();405 img.remove();
406 //replace ID so it becomes the new 'original' expression for next change406 //replace ID so it becomes the new 'original' expression for next change
407 expressionClone.attr('id', originalId);407 expressionClone.data('filename', originalId);
408 expressionClone.removeClass('expression-animating');408 expressionClone.removeClass('expression-animating');
409409
410 // Reset the expression holder min height and width410 // Reset the expression holder min height and width
@@ -1311,9 +1311,9 @@ function removeExpression() {
1311/**1311/**
1312 * Validate a character's sprites, and redraw the sprites list if not done before or forced to redraw.1312 * Validate a character's sprites, and redraw the sprites list if not done before or forced to redraw.
1313 * @param {string} character - The character to validate1313 * @param {string} character - The character to validate
1314 * @param {boolean} forceRedrawCached - Whether to force redrawing the sprites list even if it's already been drawn before1314 * @param {boolean} [forceRedrawCached=false] - Whether to force redrawing the sprites list even if it's already been drawn before
1315 */1315 */
1316async function validateImages(character, forceRedrawCached) {1316async function validateImages(character, forceRedrawCached = false) {
1317 if (!character) {1317 if (!character) {
1318 return;1318 return;
1319 }1319 }
@@ -1493,6 +1493,13 @@ async function renderFallbackExpressionPicker() {
1493 }1493 }
1494}1494}
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
1496function getCachedExpressions() {1503function getCachedExpressions() {
1497 if (!Array.isArray(expressionsList)) {1504 if (!Array.isArray(expressionsList)) {
1498 return [];1505 return [];
@@ -1558,7 +1565,14 @@ export async function getExpressionsList() {
1558 return [...result, ...extension_settings.expressions.custom].filter(onlyUnique);1565 return [...result, ...extension_settings.expressions.custom].filter(onlyUnique);
1559}1566}
15601567
1561async 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 */
1575async function setExpression(character, expression, force = false) {
1562 if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {1576 if (!isTalkingHeadEnabled() || !modules.includes('talkinghead')) {
1563 console.debug('entered setExpressions');1577 console.debug('entered setExpressions');
1564 await validateImages(character);1578 await validateImages(character);
@@ -1566,11 +1580,23 @@ async function setExpression(character, expression, force) {
1566 const prevExpressionSrc = img.attr('src');1580 const prevExpressionSrc = img.attr('src');
1567 const expressionClone = img.clone();1581 const expressionClone = img.clone();
15681582
1583 /** @type {Expression} */
1569 const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));1584 const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
1570 console.debug('checking for expression images to show..');1585 console.debug('checking for expression images to show..');
1571 if (sprite) {1586 if (sprite) {
1572 console.debug('setting expression from character images folder');1587 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
1574 if (force && isVisualNovelMode()) {1600 if (force && isVisualNovelMode()) {
1575 const context = getContext();1601 const context = getContext();
1576 const group = context.groups.find(x => x.id === context.groupId);1602 const group = context.groups.find(x => x.id === context.groupId);
@@ -1583,13 +1609,13 @@ async function setExpression(character, expression, force) {
1583 }1609 }
15841610
1585 if (groupMember.name == character) {1611 if (groupMember.name == character) {
1586 await setImage($(`.expression-holder[data-avatar="${member}"] img`), sprite.path);1612 await setImage($(`.expression-holder[data-avatar="${member}"] img`), spriteFile.imageSrc);
1587 return;1613 return;
1588 }1614 }
1589 }1615 }
1590 }1616 }
1591 //only swap expressions when necessary1617 //only swap expressions when necessary
1592 if (prevExpressionSrc !== sprite.path1618 if (prevExpressionSrc !== spriteFile.imageSrc
1593 && !img.hasClass('expression-animating')) {1619 && !img.hasClass('expression-animating')) {
1594 //clone expression1620 //clone expression
1595 expressionClone.addClass('expression-clone');1621 expressionClone.addClass('expression-clone');
@@ -1597,7 +1623,7 @@ async function setExpression(character, expression, force) {
1597 //must be made invisible to start because they share the same Z-index1623 //must be made invisible to start because they share the same Z-index
1598 expressionClone.attr('id', '').css({ opacity: 0 });1624 expressionClone.attr('id', '').css({ opacity: 0 });
1599 //add new sprite path to clone src1625 //add new sprite path to clone src
1600 expressionClone.attr('src', sprite.path);1626 expressionClone.attr('src', spriteFile.imageSrc);
1601 //add invisible clone to html1627 //add invisible clone to html
1602 expressionClone.appendTo($('#expression-holder'));1628 expressionClone.appendTo($('#expression-holder'));
16031629
@@ -1645,7 +1671,7 @@ async function setExpression(character, expression, force) {
1645 expressionClone.removeClass('default');1671 expressionClone.removeClass('default');
1646 expressionClone.off('error');1672 expressionClone.off('error');
1647 expressionClone.on('error', function () {1673 expressionClone.on('error', function () {
1648 console.debug('Expression image error', sprite.path);1674 console.debug('Expression image error', spriteFile.imageSrc);
1649 $(this).attr('src', '');1675 $(this).attr('src', '');
1650 $(this).off('error');1676 $(this).off('error');
1651 if (force && extension_settings.expressions.showDefault) {1677 if (force && extension_settings.expressions.showDefault) {
@@ -1706,7 +1732,7 @@ async function setExpression(character, expression, force) {
1706}1732}
17071733
1708function onClickExpressionImage() {1734function onClickExpressionImage() {
1709 const expression = $(this).attr('id');1735 const expression = $(this).data('expression');
1710 setSpriteSlashCommand({}, expression);1736 setSpriteSlashCommand({}, expression);
1711}1737}
17121738
@@ -1830,7 +1856,7 @@ async function onClickExpressionUpload(event) {
1830 // Prevents the expression from being set1856 // Prevents the expression from being set
1831 event.stopPropagation();1857 event.stopPropagation();
18321858
1833 const id = $(this).closest('.expression_list_item').attr('id');1859 const expression = $(this).closest('.expression_list_item').data('expression');
1834 const name = $('#image_list').data('name');1860 const name = $('#image_list').data('name');
18351861
1836 const handleExpressionUploadChange = async (e) => {1862 const handleExpressionUploadChange = async (e) => {
@@ -1840,9 +1866,20 @@ async function onClickExpressionUpload(event) {
1840 return;1866 return;
1841 }1867 }
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
1843 const formData = new FormData();1880 const formData = new FormData();
1844 formData.append('name', name);1881 formData.append('name', name);
1845 formData.append('label', id);1882 formData.append('label', expression);
1846 formData.append('avatar', file);1883 formData.append('avatar', file);
18471884
1848 await handleFileUpload('/api/sprites/upload', formData);1885 await handleFileUpload('/api/sprites/upload', formData);
@@ -1851,7 +1888,7 @@ async function onClickExpressionUpload(event) {
1851 e.target.form.reset();1888 e.target.form.reset();
18521889
1853 // In Talkinghead mode, when a new talkinghead image is uploaded, refresh the live char.1890 // In Talkinghead mode, when a new talkinghead image is uploaded, refresh the live char.
1854 if (id === 'talkinghead' && isTalkingHeadEnabled() && modules.includes('talkinghead')) {1891 if (expression === 'talkinghead' && isTalkingHeadEnabled() && modules.includes('talkinghead')) {
1855 await loadTalkingHead();1892 await loadTalkingHead();
1856 }1893 }
1857 };1894 };
@@ -1987,14 +2024,14 @@ async function onClickExpressionDelete(event) {
1987 return;2024 return;
1988 }2025 }
19892026
1990 const id = $(this).closest('.expression_list_item').attr('id');2027 const expression = $(this).closest('.expression_list_item').data('expression');
1991 const name = $('#image_list').data('name');2028 const name = $('#image_list').data('name');
19922029
1993 try {2030 try {
1994 await fetch('/api/sprites/delete', {2031 await fetch('/api/sprites/delete', {
1995 method: 'POST',2032 method: 'POST',
1996 headers: getRequestHeaders(),2033 headers: getRequestHeaders(),
1997 body: JSON.stringify({ name, label: id }),2034 body: JSON.stringify({ name, label: expression }),
1998 });2035 });
1999 } catch (error) {2036 } catch (error) {
2000 toastr.error('Failed to delete image. Try again later.');2037 toastr.error('Failed to delete image. Try again later.');
public/scripts/extensions/expressions/list-item.html+1 -1
@@ -1,5 +1,5 @@
1{{#each images}}1{{#each images}}
2<div class="expression_list_item" data-epression="{{../expression}}" data-expression-type="{{this.type}}" data-filename="{{this.fileName}}">2<div class="expression_list_item" data-expression="{{../expression}}" data-expression-type="{{this.type}}" data-filename="{{this.fileName}}">
3 <div class="expression_list_buttons">3 <div class="expression_list_buttons">
4 <div class="menu_button expression_list_upload" title="Upload image">4 <div class="menu_button expression_list_upload" title="Upload image">
5 <i class="fa-solid fa-upload"></i>5 <i class="fa-solid fa-upload"></i>