Allow searching on /sendas via tag=
| @@ -187,11 +187,18 @@ export function initDefaultSlashCommands() { | |||
| 187 | }), | 187 | }), |
| 188 | SlashCommandNamedArgument.fromProps({ | 188 | SlashCommandNamedArgument.fromProps({ |
| 189 | name: 'avatar', | 189 | name: 'avatar', |
| 190 | description: 'Optional character avatar override (Can be either avatar filename or just the character name to pull the avatar from)', | 190 | description: 'Character avatar override (Can be either avatar filename or just the character name to pull the avatar from)', |
| 191 | typeList: [ARGUMENT_TYPE.STRING], | 191 | typeList: [ARGUMENT_TYPE.STRING], |
| 192 | enumProvider: commonEnumProviders.characters('character'), | 192 | enumProvider: commonEnumProviders.characters('character'), |
| 193 | }), | 193 | }), |
| 194 | SlashCommandNamedArgument.fromProps({ | 194 | SlashCommandNamedArgument.fromProps({ |
| 195 | name: 'tag', | ||
| 196 | description: 'Supply one or more tags to filter down to the correct character for the provided name, if multiple characters have the same name. (does not apply to the avatar argument)', | ||
| 197 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 198 | enumProvider: commonEnumProviders.tagsForChar('all'), | ||
| 199 | acceptsMultiple: true, | ||
| 200 | }), | ||
| 201 | SlashCommandNamedArgument.fromProps({ | ||
| 195 | name: 'compact', | 202 | name: 'compact', |
| 196 | description: 'Use compact layout', | 203 | description: 'Use compact layout', |
| 197 | typeList: [ARGUMENT_TYPE.BOOLEAN], | 204 | typeList: [ARGUMENT_TYPE.BOOLEAN], |
| @@ -3109,6 +3116,44 @@ async function setNarratorName(_, text) { | |||
| 3109 | return ''; | 3116 | return ''; |
| 3110 | } | 3117 | } |
| 3111 | 3118 | ||
| 3119 | /** | ||
| 3120 | * Finds a character by name, with optional filtering and precedence for avatars | ||
| 3121 | * @param {string} name - The name to search for | ||
| 3122 | * @param {object} [options={}] - The options for the search | ||
| 3123 | * @param {boolean} [options.allowAvatar=false] - Whether to allow searching by avatar | ||
| 3124 | * @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive | ||
| 3125 | * @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by | ||
| 3126 | * @param {any?} [options.preferCurrentChar=null] - The current character to prefer | ||
| 3127 | * @returns {any?} - The found character or null if not found | ||
| 3128 | */ | ||
| 3129 | export function findCharByName(name, { allowAvatar = false, insensitive = true, filteredByTags = null, preferCurrentChar = null } = {}) { | ||
| 3130 | const matches = (char) => (allowAvatar && char.avatar === name) || insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name; | ||
| 3131 | |||
| 3132 | // If we have a current char and prefer it, return that if it matches - unless tags are provided, they have precedence | ||
| 3133 | if (preferCurrentChar && !filteredByTags && matches(preferCurrentChar)) { | ||
| 3134 | return preferCurrentChar; | ||
| 3135 | } | ||
| 3136 | |||
| 3137 | // Filter characters by tags if provided | ||
| 3138 | let filteredCharacters = characters; | ||
| 3139 | if (filteredByTags) { | ||
| 3140 | filteredCharacters = characters.filter(char => filteredByTags.every(tag => char.tags.includes(tag))); | ||
| 3141 | } | ||
| 3142 | |||
| 3143 | // If allowAvatar is true, search by avatar first | ||
| 3144 | if (allowAvatar) { | ||
| 3145 | const characterByAvatar = filteredCharacters.find(char => char.avatar === name); | ||
| 3146 | if (characterByAvatar) { | ||
| 3147 | return characterByAvatar; | ||
| 3148 | } | ||
| 3149 | } | ||
| 3150 | |||
| 3151 | // Search for a matching character by name | ||
| 3152 | let character = filteredCharacters.find(matches); | ||
| 3153 | |||
| 3154 | return character; | ||
| 3155 | } | ||
| 3156 | |||
| 3112 | export async function sendMessageAs(args, text) { | 3157 | export async function sendMessageAs(args, text) { |
| 3113 | if (!text) { | 3158 | if (!text) { |
| 3114 | return ''; | 3159 | return ''; |
| @@ -3150,24 +3195,21 @@ export async function sendMessageAs(args, text) { | |||
| 3150 | const chatCharacter = this_chid !== undefined ? characters[this_chid] : null; | 3195 | const chatCharacter = this_chid !== undefined ? characters[this_chid] : null; |
| 3151 | const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName; | 3196 | const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName; |
| 3152 | 3197 | ||
| 3153 | const character = equalsIgnoreCaseAndAccents(chatCharacter.name, name) ? chatCharacter : characters.find(x => equalsIgnoreCaseAndAccents(x.name, name)); | 3198 | const character = findCharByName(name, { filteredByTags: args?.tags, preferCurrentChar: chatCharacter }); |
| 3154 | 3199 | ||
| 3155 | let avatarChar = character; | 3200 | const avatarCharacter = args.avatar ? findCharByName(args.avatar) : character; |
| 3156 | if (args.avatar) { | 3201 | if (args.avatar && !avatarCharacter) { |
| 3157 | avatarChar = characters.find(x => x.avatar == args.avatar) ?? characters.find(x => equalsIgnoreCaseAndAccents(x.name, args.avatar)); | 3202 | toastr.warning(`Character for avatar ${args.avatar} not found`); |
| 3158 | if (!avatarChar) { | 3203 | return ''; |
| 3159 | toastr.warning(`Character for avatar ${args.avatar} not found`); | ||
| 3160 | return ''; | ||
| 3161 | } | ||
| 3162 | } | 3204 | } |
| 3163 | 3205 | ||
| 3164 | let force_avatar, original_avatar; | 3206 | let force_avatar, original_avatar; |
| 3165 | if (chatCharacter === avatarChar || isNeutralCharacter) { | 3207 | if (chatCharacter === avatarCharacter || isNeutralCharacter) { |
| 3166 | // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars | 3208 | // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars |
| 3167 | } | 3209 | } |
| 3168 | else if (avatarChar && avatarChar.avatar !== 'none') { | 3210 | else if (avatarCharacter && avatarCharacter.avatar !== 'none') { |
| 3169 | force_avatar = getThumbnailUrl('avatar', avatarChar.avatar); | 3211 | force_avatar = getThumbnailUrl('avatar', avatarCharacter.avatar); |
| 3170 | original_avatar = avatarChar.avatar; | 3212 | original_avatar = avatarCharacter.avatar; |
| 3171 | } | 3213 | } |
| 3172 | else { | 3214 | else { |
| 3173 | force_avatar = default_avatar; | 3215 | force_avatar = default_avatar; |
| @@ -193,7 +193,7 @@ export const commonEnumProviders = { | |||
| 193 | if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures'); | 193 | if (charName instanceof SlashCommandClosure) throw new Error('Argument \'name\' does not support closures'); |
| 194 | const key = searchCharByName(substituteParams(charName), { suppressLogging: true }); | 194 | const key = searchCharByName(substituteParams(charName), { suppressLogging: true }); |
| 195 | const assigned = key ? getTagsList(key) : []; | 195 | const assigned = key ? getTagsList(key) : []; |
| 196 | return tags.filter(it => !key || mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it)) | 196 | return tags.filter(it => mode === 'all' || mode === 'existing' && assigned.includes(it) || mode === 'not-existing' && !assigned.includes(it)) |
| 197 | .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag)); | 197 | .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag)); |
| 198 | }, | 198 | }, |
| 199 | 199 | ||