/** * User Attributes Widget * * Displays customizable RPG attribute scores with +/- adjustment buttons. * Integrates with Tracker Settings for full attribute customization. * * Features: * - Fully customizable attributes (add/remove/rename via Tracker Settings) * - Custom attribute names (e.g., "STRENGTH" instead of "STR", or add "LCK") * - Widget-level filtering (show subset of globally enabled attributes) * - +/- buttons for quick adjustments (1-20 range) * - Responsive 2-column grid layout * - Smart sizing: auto-adjusts height based on attribute count * - Bi-directional sync with Tracker Editor */ import { parseNumber } from '../widgetBase.js'; /** * Register User Attributes Widget * @param {WidgetRegistry} registry - Widget registry instance * @param {Object} dependencies - External dependencies * @param {Function} dependencies.getExtensionSettings - Get extension settings * @param {Function} dependencies.onStatsChange - Callback when stats change */ export function registerUserAttributesWidget(registry, dependencies) { const { getExtensionSettings, onStatsChange } = dependencies; registry.register('userAttributes', { name: 'User Attributes', icon: '⚔️', description: 'Customizable RPG attributes with +/- buttons (STR, DEX, etc.)', category: 'user', minSize: { w: 2, h: 2 }, defaultSize: { w: 2, h: 2 }, maxAutoSize: { w: 3, h: 5 }, // Max size for auto-arrange expansion requiresSchema: false, /** * Render widget content * @param {HTMLElement} container - Widget container * @param {Object} config - Widget configuration */ render(container, config = {}) { const settings = getExtensionSettings(); const classicStats = settings.classicStats; const trackerConfig = settings.trackerConfig?.userStats; // Get globally enabled attributes from trackerConfig const globallyEnabledAttrs = trackerConfig?.rpgAttributes ?.filter(attr => attr.enabled) .map(attr => ({ id: attr.id, name: attr.name })) || []; // If no globally enabled attrs, fall back to defaults const availableAttrs = globallyEnabledAttrs.length > 0 ? globallyEnabledAttrs : [ { id: 'str', name: 'STR' }, { id: 'dex', name: 'DEX' }, { id: 'con', name: 'CON' }, { id: 'int', name: 'INT' }, { id: 'wis', name: 'WIS' }, { id: 'cha', name: 'CHA' } ]; // Apply widget-level filter if specified (support both visibleAttrs and legacy visibleStats) let visibleAttrs = availableAttrs; const filterList = config.visibleAttrs || config.visibleStats; if (filterList && filterList.length > 0) { visibleAttrs = availableAttrs.filter(attr => filterList.includes(attr.id) ); } // Merge default config const finalConfig = { showLabels: true, ...config }; // Build stats HTML using custom names from trackerConfig const statsHtml = visibleAttrs.map(attr => `
${finalConfig.showLabels ? `${attr.name}` : ''}
${classicStats[attr.id] || 10}
`).join(''); // Render HTML const html = `
${statsHtml}
`; container.innerHTML = html; // Attach event handlers attachEventHandlers(container, settings, onStatsChange); }, /** * Get configuration options * @returns {Object} Configuration schema */ getConfig() { const settings = getExtensionSettings(); const trackerConfig = settings.trackerConfig?.userStats; // Get enabled attributes from trackerConfig for options const enabledAttrs = trackerConfig?.rpgAttributes ?.filter(attr => attr.enabled) .map(attr => ({ value: attr.id, label: attr.name })) || [ { value: 'str', label: 'STR' }, { value: 'dex', label: 'DEX' }, { value: 'con', label: 'CON' }, { value: 'int', label: 'INT' }, { value: 'wis', label: 'WIS' }, { value: 'cha', label: 'CHA' } ]; return { visibleAttrs: { type: 'multiselect', label: 'Visible Attributes', default: null, // null means "show all enabled attributes" options: enabledAttrs, description: 'Select which attributes to show in this widget (leave empty to show all enabled attributes)', hint: 'To add/remove/rename attributes globally, use Tracker Settings' }, showLabels: { type: 'boolean', label: 'Show Stat Labels', default: true } }; }, /** * Handle configuration changes * @param {HTMLElement} container - Widget container * @param {Object} newConfig - New configuration */ onConfigChange(container, newConfig) { this.render(container, newConfig); }, /** * Handle widget resize * @param {HTMLElement} container - Widget container * @param {number} newW - New width * @param {number} newH - New height */ onResize(container, newW, newH) { const statsGrid = container.querySelector('.rpg-classic-stats-grid'); if (!statsGrid) return; // Compact single-column layout for narrow widgets if (newW < 2) { statsGrid.style.gridTemplateColumns = '1fr'; } else { // 2-column grid for wider widgets statsGrid.style.gridTemplateColumns = 'repeat(2, 1fr)'; } }, /** * Calculate optimal size based on content * Used by smart auto-layout to determine ideal widget dimensions * @param {Object} config - Widget configuration * @returns {Object} Optimal size { w, h } */ getOptimalSize(config = {}) { const settings = getExtensionSettings(); const trackerConfig = settings.trackerConfig?.userStats; // Count globally enabled attributes const globallyEnabledCount = trackerConfig?.rpgAttributes ?.filter(attr => attr.enabled).length || 6; // If widget has visibleAttrs override, use that count (support legacy visibleStats too) const filterList = config.visibleAttrs || config.visibleStats; const visibleAttrCount = filterList?.length || globallyEnabledCount; // Each attribute needs ~0.35 rows in 2-column grid // For 6 attrs: 3 rows (0.5 row padding = 3.5 total) const optimalHeight = Math.ceil((visibleAttrCount / 2) * 0.7 + 0.5); return { w: 2, // Prefer 2-column grid layout h: Math.max(this.minSize.h, optimalHeight) }; } }); } /** * Attach event handlers to widget * @private */ function attachEventHandlers(container, settings, onStatsChange) { // Handle classic stat +/- buttons const increaseButtons = container.querySelectorAll('.rpg-stat-increase'); const decreaseButtons = container.querySelectorAll('.rpg-stat-decrease'); increaseButtons.forEach(btn => { btn.addEventListener('click', () => { const statName = btn.dataset.stat; const valueSpan = btn.parentElement.querySelector('.rpg-classic-stat-value'); const currentValue = parseNumber(valueSpan.textContent, 10, 1, 20); const newValue = Math.min(20, currentValue + 1); valueSpan.textContent = newValue; settings.classicStats[statName] = newValue; if (onStatsChange) { onStatsChange('classicStats', statName, newValue); } }); }); decreaseButtons.forEach(btn => { btn.addEventListener('click', () => { const statName = btn.dataset.stat; const valueSpan = btn.parentElement.querySelector('.rpg-classic-stat-value'); const currentValue = parseNumber(valueSpan.textContent, 10, 1, 20); const newValue = Math.max(1, currentValue - 1); valueSpan.textContent = newValue; settings.classicStats[statName] = newValue; if (onStatsChange) { onStatsChange('classicStats', statName, newValue); } }); }); }