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.
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-17 13:25:38 +11:00
parent bb952aecec
commit f4dfd368e1
3 changed files with 139 additions and 90 deletions
+4
View File
@@ -151,6 +151,10 @@ export function setPendingDiceRoll(roll) {
pendingDiceRoll = roll; pendingDiceRoll = roll;
} }
export function getPendingDiceRoll() {
return pendingDiceRoll;
}
export function setPanelContainer($element) { export function setPanelContainer($element) {
$panelContainer = $element; $panelContainer = $element;
} }
+113
View File
@@ -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<number>}>} 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
}
}
+22 -90
View File
@@ -8,14 +8,20 @@ import {
extensionSettings, extensionSettings,
lastGeneratedData, lastGeneratedData,
committedTrackerData, committedTrackerData,
pendingDiceRoll,
$infoBoxContainer, $infoBoxContainer,
$thoughtsContainer, $thoughtsContainer,
setPendingDiceRoll setPendingDiceRoll,
getPendingDiceRoll
} from '../../core/state.js'; } from '../../core/state.js';
import { saveSettings, saveChatData } from '../../core/persistence.js'; import { saveSettings, saveChatData } from '../../core/persistence.js';
import { renderUserStats } from '../rendering/userStats.js'; import { renderUserStats } from '../rendering/userStats.js';
import { updateChatThoughts } from '../rendering/thoughts.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 * Modern DiceModal ES6 Class
@@ -283,16 +289,17 @@ export function setupDiceRoller() {
// Roll dice button // Roll dice button
$('#rpg-dice-roll-btn').on('click', async function() { $('#rpg-dice-roll-btn').on('click', async function() {
await rollDice(); await rollDiceCore(diceModal);
}); });
// Save roll button (closes popup and saves the roll) // Save roll button (closes popup and saves the roll)
$('#rpg-dice-save-btn').on('click', function() { $('#rpg-dice-save-btn').on('click', function() {
// Save the pending roll // Save the pending roll
if (pendingDiceRoll) { const roll = getPendingDiceRoll();
extensionSettings.lastDiceRoll = pendingDiceRoll; if (roll) {
extensionSettings.lastDiceRoll = roll;
saveSettings(); saveSettings();
updateDiceDisplay(); updateDiceDisplayCore();
setPendingDiceRoll(null); setPendingDiceRoll(null);
} }
closeDicePopup(); closeDicePopup();
@@ -301,14 +308,14 @@ export function setupDiceRoller() {
// Reset on Enter key // Reset on Enter key
$('#rpg-dice-count, #rpg-dice-sides').on('keypress', function(e) { $('#rpg-dice-count, #rpg-dice-sides').on('keypress', function(e) {
if (e.which === 13) { if (e.which === 13) {
rollDice(); rollDiceCore(diceModal);
} }
}); });
// Clear dice roll button // Clear dice roll button
$('#rpg-clear-dice').on('click', function(e) { $('#rpg-clear-dice').on('click', function(e) {
e.stopPropagation(); // Prevent opening the dice popup e.stopPropagation(); // Prevent opening the dice popup
clearDiceRoll(); clearDiceRollCore();
}); });
return diceModal; return diceModal;
@@ -402,7 +409,7 @@ export function setupSettingsPopup() {
// Re-render user stats and dice display // Re-render user stats and dice display
renderUserStats(); renderUserStats();
updateDiceDisplay(); updateDiceDisplayCore();
updateChatThoughts(); // Clear the thought bubble in chat updateChatThoughts(); // Clear the thought bubble in chat
// console.log('[RPG Companion] Chat cache cleared'); // console.log('[RPG Companion] Chat cache cleared');
@@ -462,101 +469,26 @@ export function applyCustomThemeToPopup() {
/** /**
* Clears the last dice roll. * Clears the last dice roll.
* Backwards compatible wrapper for dice module.
*/ */
export function clearDiceRoll() { export function clearDiceRoll() {
extensionSettings.lastDiceRoll = null; clearDiceRollCore();
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: [] };
}
} }
/** /**
* Updates the dice display in the sidebar. * Updates the dice display in the sidebar.
* Backwards compatible wrapper for dice module.
*/ */
export function updateDiceDisplay() { export function updateDiceDisplay() {
const lastRoll = extensionSettings.lastDiceRoll; updateDiceDisplayCore();
if (lastRoll) {
$('#rpg-last-roll-text').text(`Last Roll (${lastRoll.formula}): ${lastRoll.total}`);
} else {
$('#rpg-last-roll-text').text('Last Roll: None');
}
} }
/** /**
* Adds the Roll Dice quick reply button. * Adds the Roll Dice quick reply button.
* Backwards compatible wrapper for dice module.
*/ */
export function addDiceQuickReply() { export function addDiceQuickReply() {
// Create quick reply button if Quick Replies exist addDiceQuickReplyCore();
if (window.quickReplyApi) {
// Quick Reply API integration would go here
// For now, the dice display in the sidebar serves as the button
}
} }
/** /**