From f4dfd368e1f45e4717ae70a1f457cfed3d7c4ecc Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Fri, 17 Oct 2025 13:25:38 +1100 Subject: [PATCH] refactor(features): extract dice system to standalone module Extract dice rolling functionality from modals.js into dedicated feature module at src/systems/features/dice.js. This includes: - rollDice() - core rolling logic with animation - executeRollCommand() - dice notation parser - updateDiceDisplay() - sidebar display updates - clearDiceRoll() - clear last roll - addDiceQuickReply() - quick reply integration Also fixes ES6 module binding issue with pendingDiceRoll by adding getPendingDiceRoll() getter function in state.js to ensure correct value retrieval across module boundaries. Reduces modals.js from 568 to 499 lines (-69 lines). Creates dice.js with 113 lines of focused dice functionality. --- src/core/state.js | 4 ++ src/systems/features/dice.js | 113 +++++++++++++++++++++++++++++++++++ src/systems/ui/modals.js | 112 +++++++--------------------------- 3 files changed, 139 insertions(+), 90 deletions(-) create mode 100644 src/systems/features/dice.js diff --git a/src/core/state.js b/src/core/state.js index 530d590..19fffe3 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -151,6 +151,10 @@ export function setPendingDiceRoll(roll) { pendingDiceRoll = roll; } +export function getPendingDiceRoll() { + return pendingDiceRoll; +} + export function setPanelContainer($element) { $panelContainer = $element; } diff --git a/src/systems/features/dice.js b/src/systems/features/dice.js new file mode 100644 index 0000000..92e691b --- /dev/null +++ b/src/systems/features/dice.js @@ -0,0 +1,113 @@ +/** + * Dice System Module + * Handles dice rolling logic, display updates, and quick reply integration + */ + +import { + extensionSettings, + pendingDiceRoll, + setPendingDiceRoll +} from '../../core/state.js'; +import { saveSettings } from '../../core/persistence.js'; + +/** + * Rolls the dice and displays result. + * Works with the DiceModal class for UI updates. + * @param {DiceModal} diceModal - The DiceModal instance + */ +export async function rollDice(diceModal) { + if (!diceModal) return; + + const count = parseInt(String($('#rpg-dice-count').val())) || 1; + const sides = parseInt(String($('#rpg-dice-sides').val())) || 20; + + // Start rolling animation + diceModal.startRolling(); + + // Wait for animation (simulate rolling) + await new Promise(resolve => setTimeout(resolve, 1200)); + + // Execute /roll command + const rollCommand = `/roll ${count}d${sides}`; + const rollResult = await executeRollCommand(rollCommand); + + // Parse result + const total = rollResult.total || 0; + const rolls = rollResult.rolls || []; + + // Store result temporarily (not saved until "Save Roll" is clicked) + setPendingDiceRoll({ + formula: `${count}d${sides}`, + total: total, + rolls: rolls, + timestamp: Date.now() + }); + + // Show result + diceModal.showResult(total, rolls); + + // Don't update sidebar display yet - only update when user clicks "Save Roll" +} + +/** + * Executes a /roll command and returns the result. + * @param {string} command - The roll command (e.g., "/roll 2d20") + * @returns {Promise<{total: number, rolls: Array}>} The roll result + */ +export async function executeRollCommand(command) { + try { + // Parse the dice notation (e.g., "2d20") + const match = command.match(/(\d+)d(\d+)/); + if (!match) { + return { total: 0, rolls: [] }; + } + + const count = parseInt(match[1]); + const sides = parseInt(match[2]); + const rolls = []; + let total = 0; + + for (let i = 0; i < count; i++) { + const roll = Math.floor(Math.random() * sides) + 1; + rolls.push(roll); + total += roll; + } + + return { total, rolls }; + } catch (error) { + console.error('[RPG Companion] Error rolling dice:', error); + return { total: 0, rolls: [] }; + } +} + +/** + * Updates the dice display in the sidebar. + */ +export function updateDiceDisplay() { + const lastRoll = extensionSettings.lastDiceRoll; + if (lastRoll) { + $('#rpg-last-roll-text').text(`Last Roll (${lastRoll.formula}): ${lastRoll.total}`); + } else { + $('#rpg-last-roll-text').text('Last Roll: None'); + } +} + +/** + * Clears the last dice roll. + */ +export function clearDiceRoll() { + extensionSettings.lastDiceRoll = null; + saveSettings(); + updateDiceDisplay(); +} + +/** + * Adds the Roll Dice quick reply button. + */ +export function addDiceQuickReply() { + // Create quick reply button if Quick Replies exist + if (window.quickReplyApi) { + // Quick Reply API integration would go here + // For now, the dice display in the sidebar serves as the button + } +} diff --git a/src/systems/ui/modals.js b/src/systems/ui/modals.js index e2ed0dc..51b1827 100644 --- a/src/systems/ui/modals.js +++ b/src/systems/ui/modals.js @@ -8,14 +8,20 @@ import { extensionSettings, lastGeneratedData, committedTrackerData, - pendingDiceRoll, $infoBoxContainer, $thoughtsContainer, - setPendingDiceRoll + setPendingDiceRoll, + getPendingDiceRoll } from '../../core/state.js'; import { saveSettings, saveChatData } from '../../core/persistence.js'; import { renderUserStats } from '../rendering/userStats.js'; import { updateChatThoughts } from '../rendering/thoughts.js'; +import { + rollDice as rollDiceCore, + clearDiceRoll as clearDiceRollCore, + updateDiceDisplay as updateDiceDisplayCore, + addDiceQuickReply as addDiceQuickReplyCore +} from '../features/dice.js'; /** * Modern DiceModal ES6 Class @@ -283,16 +289,17 @@ export function setupDiceRoller() { // Roll dice button $('#rpg-dice-roll-btn').on('click', async function() { - await rollDice(); + await rollDiceCore(diceModal); }); // Save roll button (closes popup and saves the roll) $('#rpg-dice-save-btn').on('click', function() { // Save the pending roll - if (pendingDiceRoll) { - extensionSettings.lastDiceRoll = pendingDiceRoll; + const roll = getPendingDiceRoll(); + if (roll) { + extensionSettings.lastDiceRoll = roll; saveSettings(); - updateDiceDisplay(); + updateDiceDisplayCore(); setPendingDiceRoll(null); } closeDicePopup(); @@ -301,14 +308,14 @@ export function setupDiceRoller() { // Reset on Enter key $('#rpg-dice-count, #rpg-dice-sides').on('keypress', function(e) { if (e.which === 13) { - rollDice(); + rollDiceCore(diceModal); } }); // Clear dice roll button $('#rpg-clear-dice').on('click', function(e) { e.stopPropagation(); // Prevent opening the dice popup - clearDiceRoll(); + clearDiceRollCore(); }); return diceModal; @@ -402,7 +409,7 @@ export function setupSettingsPopup() { // Re-render user stats and dice display renderUserStats(); - updateDiceDisplay(); + updateDiceDisplayCore(); updateChatThoughts(); // Clear the thought bubble in chat // console.log('[RPG Companion] Chat cache cleared'); @@ -462,101 +469,26 @@ export function applyCustomThemeToPopup() { /** * Clears the last dice roll. + * Backwards compatible wrapper for dice module. */ export function clearDiceRoll() { - extensionSettings.lastDiceRoll = null; - saveSettings(); - updateDiceDisplay(); -} - -/** - * Rolls the dice and displays result. - * Refactored to use DiceModal class. - */ -async function rollDice() { - if (!diceModal) return; - - const count = parseInt(String($('#rpg-dice-count').val())) || 1; - const sides = parseInt(String($('#rpg-dice-sides').val())) || 20; - - // Start rolling animation - diceModal.startRolling(); - - // Wait for animation (simulate rolling) - await new Promise(resolve => setTimeout(resolve, 1200)); - - // Execute /roll command - const rollCommand = `/roll ${count}d${sides}`; - const rollResult = await executeRollCommand(rollCommand); - - // Parse result - const total = rollResult.total || 0; - const rolls = rollResult.rolls || []; - - // Store result temporarily (not saved until "Save Roll" is clicked) - setPendingDiceRoll({ - formula: `${count}d${sides}`, - total: total, - rolls: rolls, - timestamp: Date.now() - }); - - // Show result - diceModal.showResult(total, rolls); - - // Don't update sidebar display yet - only update when user clicks "Save Roll" -} - -/** - * Executes a /roll command and returns the result. - */ -async function executeRollCommand(command) { - try { - // Parse the dice notation (e.g., "2d20") - const match = command.match(/(\d+)d(\d+)/); - if (!match) { - return { total: 0, rolls: [] }; - } - - const count = parseInt(match[1]); - const sides = parseInt(match[2]); - const rolls = []; - let total = 0; - - for (let i = 0; i < count; i++) { - const roll = Math.floor(Math.random() * sides) + 1; - rolls.push(roll); - total += roll; - } - - return { total, rolls }; - } catch (error) { - console.error('[RPG Companion] Error rolling dice:', error); - return { total: 0, rolls: [] }; - } + clearDiceRollCore(); } /** * Updates the dice display in the sidebar. + * Backwards compatible wrapper for dice module. */ export function updateDiceDisplay() { - const lastRoll = extensionSettings.lastDiceRoll; - if (lastRoll) { - $('#rpg-last-roll-text').text(`Last Roll (${lastRoll.formula}): ${lastRoll.total}`); - } else { - $('#rpg-last-roll-text').text('Last Roll: None'); - } + updateDiceDisplayCore(); } /** * Adds the Roll Dice quick reply button. + * Backwards compatible wrapper for dice module. */ export function addDiceQuickReply() { - // Create quick reply button if Quick Replies exist - if (window.quickReplyApi) { - // Quick Reply API integration would go here - // For now, the dice display in the sidebar serves as the button - } + addDiceQuickReplyCore(); } /**