Add `/reroll-pick` command to reset `{{pick}}` macro (#5049) * feat(macros): Add `/reroll-pick` command to reset `{{pick}}` macro choices - Added `/reroll-pick` slash command to change the seed for all `{{pick}}` macros in current chat - Command accepts optional numeric seed value, otherwise increments current seed by 1 - Updated `{{pick}}` macro to use `pick_reroll_seed` from chat metadata in seed calculation - Updated `{{pick}}` macro description to mention reroll capability - Added comprehensive help text with examples for `/reroll-pick` command - Updated tests * lint fix * fix(macros): exclude null reroll seed from {{pick}} hash calculation - Changed reroll seed default from 0 to null when not set - Filter out null values from combined seed string to avoid including "-0" suffix - Updated both core macro implementation and tests to match new behavior * Use strict null comparison --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -342,7 +342,12 @@ export function registerCoreMacros() { | |||
| 342 | MacroRegistry.registerMacro('pick', { | 342 | MacroRegistry.registerMacro('pick', { |
| 343 | category: MacroCategory.RANDOM, | 343 | category: MacroCategory.RANDOM, |
| 344 | list: true, | 344 | list: true, |
| 345 | description: 'Picks a random item from a list, but keeps the choice stable for a given chat and macro position.', | 345 | description: 'Picks a random item from a list, but keeps the choice stable for a given chat and macro position. Can be rerolled via /reroll-pick slash command.', |
| 346 | // TODO: add expanded documentation once HTML details are supported | ||
| 347 | // descriptionDetails: ` | ||
| 348 | // <p>Picks a random item from a list, but keeps the choice stable for a given chat and macro position.</p> | ||
| 349 | // <p>The choice can be reset per chat using the <code>/reroll-pick</code> slash command.</p> | ||
| 350 | // `, | ||
| 346 | returns: 'Stable randomly selected item from the list.', | 351 | returns: 'Stable randomly selected item from the list.', |
| 347 | exampleUsage: ['{{pick::blonde::brown::red::black::blue}}'], | 352 | exampleUsage: ['{{pick::blonde::brown::red::black::blue}}'], |
| 348 | handler: ({ list, globalOffset, env }) => { | 353 | handler: ({ list, globalOffset, env }) => { |
| @@ -369,7 +374,10 @@ export function registerCoreMacros() { | |||
| 369 | // nested inside arguments or scoped content | 374 | // nested inside arguments or scoped content |
| 370 | const offset = globalOffset; | 375 | const offset = globalOffset; |
| 371 | 376 | ||
| 372 | const combinedSeedString = `${chatIdHash}-${rawContentHash}-${offset}`; | 377 | // Reroll seed allows users to reset all picks in the chat via /reroll-pick command |
| 378 | const rerollSeed = chat_metadata.pick_reroll_seed || null; | ||
| 379 | |||
| 380 | const combinedSeedString = [chatIdHash, rawContentHash, offset, rerollSeed].filter(it => it !== null).join('-'); | ||
| 373 | const finalSeed = getStringHash(combinedSeedString); | 381 | const finalSeed = getStringHash(combinedSeedString); |
| 374 | const rng = seedrandom(String(finalSeed)); | 382 | const rng = seedrandom(String(finalSeed)); |
| 375 | const randomIndex = Math.floor(rng() * list.length); | 383 | const randomIndex = Math.floor(rng() * list.length); |
| @@ -3103,6 +3103,48 @@ export function initDefaultSlashCommands() { | |||
| 3103 | }, | 3103 | }, |
| 3104 | })); | 3104 | })); |
| 3105 | 3105 | ||
| 3106 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | ||
| 3107 | name: 'reroll-pick', | ||
| 3108 | callback: (_, value) => { | ||
| 3109 | const currentSeed = chat_metadata.pick_reroll_seed ?? 0; | ||
| 3110 | const parsedValue = value ? parseInt(String(value), 10) : NaN; | ||
| 3111 | |||
| 3112 | if (!isNaN(parsedValue)) { | ||
| 3113 | chat_metadata.pick_reroll_seed = parsedValue; | ||
| 3114 | } else { | ||
| 3115 | chat_metadata.pick_reroll_seed = currentSeed + 1; | ||
| 3116 | } | ||
| 3117 | |||
| 3118 | saveMetadataDebounced(); | ||
| 3119 | return String(chat_metadata.pick_reroll_seed); | ||
| 3120 | }, | ||
| 3121 | returns: t`The new reroll seed value.`, | ||
| 3122 | unnamedArgumentList: [ | ||
| 3123 | SlashCommandArgument.fromProps({ | ||
| 3124 | description: t`Optional seed value to set. If not provided, increments current seed by 1.`, | ||
| 3125 | typeList: [ARGUMENT_TYPE.NUMBER], | ||
| 3126 | }), | ||
| 3127 | ], | ||
| 3128 | helpString: ` | ||
| 3129 | <div> | ||
| 3130 | ${t`Rerolls all <code>{{pick}}</code> macro choices in the current chat.`} | ||
| 3131 | </div> | ||
| 3132 | <div> | ||
| 3133 | ${t`The <code>{{pick}}</code> macro normally keeps stable choices per chat. This command changes the seed used for all picks, causing them to resolve to (possibly) different values.`} | ||
| 3134 | </div> | ||
| 3135 | <div> | ||
| 3136 | ${t`If a number is provided, sets the seed to that value. Otherwise, increments the current seed by 1.`} | ||
| 3137 | </div> | ||
| 3138 | <div> | ||
| 3139 | <strong>${t`Example:`}</strong> | ||
| 3140 | <ul> | ||
| 3141 | <li><pre><code>/reroll-pick</code></pre> ${t`Increments the seed by 1.`}</li> | ||
| 3142 | <li><pre><code>/reroll-pick 5</code></pre> ${t`Sets the seed to 5.`}</li> | ||
| 3143 | </ul> | ||
| 3144 | </div> | ||
| 3145 | `, | ||
| 3146 | })); | ||
| 3147 | |||
| 3106 | registerVariableCommands(); | 3148 | registerVariableCommands(); |
| 3107 | } | 3149 | } |
| 3108 | 3150 | ||
| @@ -664,7 +664,8 @@ test.describe('MacroEngine', () => { | |||
| 664 | const chatIdHash = chat_metadata.chat_id_hash ?? 0; | 664 | const chatIdHash = chat_metadata.chat_id_hash ?? 0; |
| 665 | const rawContentHash = env.contentHash; | 665 | const rawContentHash = env.contentHash; |
| 666 | const offset = globalOffset; | 666 | const offset = globalOffset; |
| 667 | const combinedSeedString = `${chatIdHash}-${rawContentHash}-${offset}`; | 667 | const rerollSeed = chat_metadata.pick_reroll_seed || null; |
| 668 | const combinedSeedString = [chatIdHash, rawContentHash, offset, rerollSeed].filter(it => it !== null).join('-'); | ||
| 668 | // Return both the seed and what would be picked for validation | 669 | // Return both the seed and what would be picked for validation |
| 669 | const finalSeed = getStringHash(combinedSeedString); | 670 | const finalSeed = getStringHash(combinedSeedString); |
| 670 | const rng = seedrandom(String(finalSeed)); | 671 | const rng = seedrandom(String(finalSeed)); |