Files
rpg-companion-sillytavern/src/systems/dashboard/widgets/userAttributesWidget.js
T
Lucas 'Paperboy' Rose-Winters 724281b6bb feat(dashboard): integrate User Attributes widget with trackerConfig.rpgAttributes
Integrates the User Attributes Widget with upstream's new RPG attributes
customization system, enabling full attribute customization (add/remove/rename)
with bi-directional sync between widget and Tracker Editor.

Changes to userAttributesWidget.js:

1. render() method (lines 44-106):
   - Read from trackerConfig.userStats.rpgAttributes (not hardcoded)
   - Filter to enabled attributes only
   - Use custom attr.name for labels (e.g., "STRENGTH" vs "STR")
   - Support widget-level visibleAttrs filtering
   - Support legacy visibleStats config for backward compat
   - Fallback to default 6 attributes if no config

2. getConfig() method (lines 112-143):
   - Dynamically generate options from enabled attributes
   - Changed visibleStats → visibleAttrs (with legacy support)
   - Set default to null (show all enabled attributes)
   - Add hint: "To add/remove/rename attributes globally, use Tracker Settings"

3. getOptimalSize() method (lines 179-199):
   - Calculate height based on enabled attribute count (not hardcoded 6)
   - Respect widget-level visibleAttrs override if specified
   - Support legacy visibleStats parameter

4. Widget description updated:
   - Header docs: Added customization features, bi-directional sync
   - Registry description: "Customizable RPG attributes" instead of "Classic RPG stats"

Changes to dashboardManager.js:

1. shouldWidgetBeRemoved() (lines 1976-1984):
   - Add 'userAttributes' removal rule
   - Remove if showRPGAttributes === false
   - Remove if all attributes disabled

2. detectConfigChanges() (lines 1743-1752):
   - Detect when RPG Attributes section re-enabled
   - Detect when attributes re-enabled
   - Auto-add widget when conditions met

Integration Benefits:
 Custom attribute names (e.g., "STRENGTH", "AGILITY", "LUCK")
 Add custom attributes (e.g., "LCK", "PER", "APP")
 Remove unwanted default attributes
 Widget auto-updates when tracker config changes
 Widget auto-removed when section/attrs disabled
 Widget auto-added when section/attrs re-enabled
 Widget-level filtering (show subset of enabled attrs)
 Backward compatible with existing dashboards

Testing Required:
- Widget renders with default attributes
- Widget respects custom attribute names
- Widget supports custom attributes (e.g., adding "LCK")
- Widget removed when section disabled
- Widget re-added when section re-enabled
- +/- buttons work with custom attributes
- AI prompts use custom attribute names

Follows pattern from: userStatsWidget.js (lines 51-78)
Related: commit a02be34 (upstream merge)
2025-11-04 10:00:07 +11:00

247 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 => `
<div class="rpg-classic-stat" data-stat="${attr.id}">
${finalConfig.showLabels ? `<span class="rpg-classic-stat-label">${attr.name}</span>` : ''}
<div class="rpg-classic-stat-buttons">
<button class="rpg-classic-stat-btn rpg-stat-decrease" data-stat="${attr.id}"></button>
<span class="rpg-classic-stat-value">${classicStats[attr.id] || 10}</span>
<button class="rpg-classic-stat-btn rpg-stat-increase" data-stat="${attr.id}">+</button>
</div>
</div>
`).join('');
// Render HTML
const html = `
<div class="rpg-classic-stats">
<div class="rpg-classic-stats-grid">
${statsHtml}
</div>
</div>
`;
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);
}
});
});
}