fix: several issues
This commit is contained in:
@@ -44,8 +44,6 @@ import { registerAllEvents } from './src/core/events.js';
|
||||
|
||||
// Generation & Parsing modules
|
||||
import {
|
||||
generateTrackerExample,
|
||||
generateTrackerInstructions,
|
||||
generateContextualSummary,
|
||||
generateRPGPromptText,
|
||||
generateSeparateUpdatePrompt
|
||||
@@ -121,7 +119,7 @@ import { setupClassicStatsButtons } from './src/systems/features/classicStats.js
|
||||
import { ensureHtmlCleaningRegex, detectConflictingRegexScripts } from './src/systems/features/htmlCleaning.js';
|
||||
import { setupMemoryRecollectionButton, updateMemoryRecollectionButton } from './src/systems/features/memoryRecollection.js';
|
||||
import { initLorebookLimiter } from './src/systems/features/lorebookLimiter.js';
|
||||
import { DEFAULT_HTML_PROMPT, DEFAULT_TRACKER_PROMPT } from './src/systems/generation/promptBuilder.js';
|
||||
import { DEFAULT_HTML_PROMPT, DEFAULT_JSON_TRACKER_PROMPT } from './src/systems/generation/promptBuilder.js';
|
||||
|
||||
// Integration modules
|
||||
import {
|
||||
@@ -406,7 +404,7 @@ async function initUI() {
|
||||
|
||||
$('#rpg-restore-default-html-prompt').on('click', function() {
|
||||
extensionSettings.customHtmlPrompt = '';
|
||||
$('#rpg-custom-html-prompt').val('');
|
||||
$('#rpg-custom-html-prompt').val(DEFAULT_HTML_PROMPT);
|
||||
saveSettings();
|
||||
toastr.success('HTML prompt restored to default');
|
||||
});
|
||||
@@ -419,7 +417,7 @@ async function initUI() {
|
||||
|
||||
$('#rpg-restore-default-tracker-prompt').on('click', function() {
|
||||
extensionSettings.customTrackerPrompt = '';
|
||||
$('#rpg-custom-tracker-prompt').val('');
|
||||
$('#rpg-custom-tracker-prompt').val(DEFAULT_JSON_TRACKER_PROMPT);
|
||||
saveSettings();
|
||||
toastr.success('Tracker prompt restored to default');
|
||||
});
|
||||
@@ -536,7 +534,7 @@ async function initUI() {
|
||||
$('#rpg-custom-html-prompt').val(extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT);
|
||||
|
||||
// Set default tracker prompt as actual text if no custom prompt exists
|
||||
$('#rpg-custom-tracker-prompt').val(extensionSettings.customTrackerPrompt || DEFAULT_TRACKER_PROMPT);
|
||||
$('#rpg-custom-tracker-prompt').val(extensionSettings.customTrackerPrompt || DEFAULT_JSON_TRACKER_PROMPT);
|
||||
|
||||
$('#rpg-toggle-plot-buttons').prop('checked', extensionSettings.enablePlotButtons);
|
||||
$('#rpg-toggle-animations').prop('checked', extensionSettings.enableAnimations);
|
||||
|
||||
+121
-19
@@ -99,6 +99,11 @@ export function loadSettings() {
|
||||
migrateToTrackerConfig();
|
||||
saveSettings(); // Persist migration
|
||||
}
|
||||
|
||||
// Migrate to new stats/skills format with descriptions
|
||||
if (migrateStatsAndSkillsFormat()) {
|
||||
saveSettings(); // Persist migration
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[RPG Companion] Error loading settings:', error);
|
||||
console.error('[RPG Companion] Error details:', error.message, error.stack);
|
||||
@@ -194,7 +199,7 @@ export function updateMessageSwipeData() {
|
||||
*/
|
||||
export function loadChatData() {
|
||||
if (!chat_metadata || !chat_metadata.rpg_companion) {
|
||||
// Reset to defaults if no data exists
|
||||
// Reset to defaults if no data exists (new chat)
|
||||
updateExtensionSettings({
|
||||
userStats: {
|
||||
health: 100,
|
||||
@@ -215,6 +220,22 @@ export function loadChatData() {
|
||||
quests: {
|
||||
main: "None",
|
||||
optional: []
|
||||
},
|
||||
// Reset structured data fields
|
||||
inventoryV3: {
|
||||
onPerson: [],
|
||||
stored: {},
|
||||
assets: [],
|
||||
simplified: []
|
||||
},
|
||||
skillsV2: {},
|
||||
skillsData: {},
|
||||
skillAbilityLinks: {},
|
||||
charactersData: [],
|
||||
infoBoxData: null,
|
||||
questsV2: {
|
||||
main: null,
|
||||
optional: []
|
||||
}
|
||||
});
|
||||
setLastGeneratedData({
|
||||
@@ -414,12 +435,12 @@ function migrateToTrackerConfig() {
|
||||
customStats: [],
|
||||
showRPGAttributes: true,
|
||||
rpgAttributes: [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
],
|
||||
statusSection: {
|
||||
enabled: true,
|
||||
@@ -482,12 +503,12 @@ function migrateToTrackerConfig() {
|
||||
if (extensionSettings.trackerConfig.userStats.showRPGAttributes !== undefined) {
|
||||
const shouldShow = extensionSettings.trackerConfig.userStats.showRPGAttributes;
|
||||
extensionSettings.trackerConfig.userStats.rpgAttributes = [
|
||||
{ id: 'str', name: 'STR', enabled: shouldShow },
|
||||
{ id: 'dex', name: 'DEX', enabled: shouldShow },
|
||||
{ id: 'con', name: 'CON', enabled: shouldShow },
|
||||
{ id: 'int', name: 'INT', enabled: shouldShow },
|
||||
{ id: 'wis', name: 'WIS', enabled: shouldShow },
|
||||
{ id: 'cha', name: 'CHA', enabled: shouldShow }
|
||||
{ id: 'str', name: 'STR', description: '', enabled: shouldShow },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: shouldShow },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: shouldShow },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: shouldShow },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: shouldShow },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: shouldShow }
|
||||
];
|
||||
delete extensionSettings.trackerConfig.userStats.showRPGAttributes;
|
||||
console.log('[RPG Companion] Migrated showRPGAttributes to rpgAttributes array');
|
||||
@@ -496,12 +517,12 @@ function migrateToTrackerConfig() {
|
||||
// Ensure rpgAttributes exists even if no migration was needed
|
||||
if (!extensionSettings.trackerConfig.userStats.rpgAttributes) {
|
||||
extensionSettings.trackerConfig.userStats.rpgAttributes = [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
];
|
||||
}
|
||||
|
||||
@@ -584,3 +605,84 @@ function migrateToTrackerConfig() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates stats and skills to new format with description fields.
|
||||
* - customStats: adds description field
|
||||
* - rpgAttributes: adds description field
|
||||
* - skillsSection.customFields: converts from string array to object array
|
||||
* @returns {boolean} true if any migration was performed
|
||||
*/
|
||||
function migrateStatsAndSkillsFormat() {
|
||||
let migrated = false;
|
||||
|
||||
if (!extensionSettings.trackerConfig?.userStats) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const userStats = extensionSettings.trackerConfig.userStats;
|
||||
|
||||
// Migrate customStats - add description if missing
|
||||
if (userStats.customStats) {
|
||||
for (const stat of userStats.customStats) {
|
||||
if (stat && typeof stat === 'object' && stat.description === undefined) {
|
||||
stat.description = '';
|
||||
migrated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate rpgAttributes - add description if missing
|
||||
if (userStats.rpgAttributes) {
|
||||
for (const attr of userStats.rpgAttributes) {
|
||||
if (attr && typeof attr === 'object' && attr.description === undefined) {
|
||||
attr.description = '';
|
||||
migrated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate skillsSection.customFields - convert string array to object array
|
||||
if (userStats.skillsSection?.customFields) {
|
||||
const oldFields = userStats.skillsSection.customFields;
|
||||
const hasOldFormat = oldFields.some(f => typeof f === 'string');
|
||||
|
||||
if (hasOldFormat) {
|
||||
console.log('[RPG Companion] Migrating skill categories to new format');
|
||||
userStats.skillsSection.customFields = oldFields.map((field, index) => {
|
||||
if (typeof field === 'string') {
|
||||
return {
|
||||
id: 'skill_' + Date.now() + '_' + index,
|
||||
name: field,
|
||||
description: '',
|
||||
enabled: true
|
||||
};
|
||||
}
|
||||
// Already an object, ensure it has all fields
|
||||
return {
|
||||
id: field.id || 'skill_' + Date.now() + '_' + index,
|
||||
name: field.name || 'Skill',
|
||||
description: field.description || '',
|
||||
enabled: field.enabled !== false
|
||||
};
|
||||
});
|
||||
migrated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate character stats - add description if missing
|
||||
if (extensionSettings.trackerConfig?.presentCharacters?.characterStats?.customStats) {
|
||||
for (const stat of extensionSettings.trackerConfig.presentCharacters.characterStats.customStats) {
|
||||
if (stat && typeof stat === 'object' && stat.description === undefined) {
|
||||
stat.description = '';
|
||||
migrated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (migrated) {
|
||||
console.log('[RPG Companion] Stats/skills format migration complete');
|
||||
}
|
||||
|
||||
return migrated;
|
||||
}
|
||||
|
||||
+19
-16
@@ -111,22 +111,22 @@ export let extensionSettings = {
|
||||
userStats: {
|
||||
// Array of custom stats (allows add/remove/rename)
|
||||
customStats: [
|
||||
{ id: 'health', name: 'Health', enabled: true },
|
||||
{ id: 'satiety', name: 'Satiety', enabled: true },
|
||||
{ id: 'energy', name: 'Energy', enabled: true },
|
||||
{ id: 'hygiene', name: 'Hygiene', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', enabled: true }
|
||||
{ id: 'health', name: 'Health', description: '', enabled: true },
|
||||
{ id: 'satiety', name: 'Satiety', description: '', enabled: true },
|
||||
{ id: 'energy', name: 'Energy', description: '', enabled: true },
|
||||
{ id: 'hygiene', name: 'Hygiene', description: '', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', description: '', enabled: true }
|
||||
],
|
||||
// RPG Attributes (customizable D&D-style attributes)
|
||||
showRPGAttributes: true,
|
||||
alwaysSendAttributes: false, // If true, always send attributes; if false, only send with dice rolls
|
||||
rpgAttributes: [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
],
|
||||
// Status section config
|
||||
statusSection: {
|
||||
@@ -134,11 +134,14 @@ export let extensionSettings = {
|
||||
showMoodEmoji: true,
|
||||
customFields: ['Conditions'] // User can edit what to track
|
||||
},
|
||||
// Optional skills field
|
||||
// Skills section config - array of skill categories
|
||||
skillsSection: {
|
||||
enabled: false,
|
||||
label: 'Skills', // User-editable
|
||||
customFields: [] // Array of skill names
|
||||
label: 'Skills', // User-editable section label
|
||||
customFields: [
|
||||
// Each skill category has id, name, description, enabled
|
||||
// Example: { id: 'combat', name: 'Combat', description: 'Fighting and weapon abilities', enabled: true }
|
||||
]
|
||||
}
|
||||
},
|
||||
infoBox: {
|
||||
@@ -180,8 +183,8 @@ export let extensionSettings = {
|
||||
characterStats: {
|
||||
enabled: false,
|
||||
customStats: [
|
||||
{ id: 'health', name: 'Health', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', enabled: true }
|
||||
{ id: 'health', name: 'Health', description: '', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', description: '', enabled: true }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -37,7 +37,7 @@
|
||||
"template.settingsModal.display.enableItemSkillLinks": "Enable Item-Skill Links",
|
||||
"template.settingsModal.display.enableItemSkillLinksNote": "Items can grant skills. Removing an item unlinks or removes the skill.",
|
||||
"template.settingsModal.display.deleteSkillWithItem": "Delete skill when item removed",
|
||||
"template.settingsModal.display.deleteSkillWithItemNote": "When disabled (default), removing an item just unlinks the skill. When enabled, the skill is deleted.",
|
||||
"template.settingsModal.display.deleteSkillWithItemNote": "When disabled, removing an item just unlinks the skill. When enabled, the skill is deleted.",
|
||||
"template.settingsModal.display.showQuests": "Show Quests",
|
||||
"template.settingsModal.display.showThoughtsInChat": "Show Thoughts in Chat",
|
||||
"template.settingsModal.display.showThoughtsInChatNote": "Display character thoughts as overlay bubbles next to their messages",
|
||||
@@ -96,7 +96,8 @@
|
||||
"template.trackerEditorModal.userStatsTab.enableSkillsSection": "Enable Skills Section",
|
||||
"template.trackerEditorModal.userStatsTab.skillsInSeparateTabNote": "Skills are displayed in a separate tab. This toggle only affects the Status section.",
|
||||
"template.trackerEditorModal.userStatsTab.skillsLabelLabel": "Skills Label:",
|
||||
"template.trackerEditorModal.userStatsTab.skillsListLabel": "Skills List (comma-separated):",
|
||||
"template.trackerEditorModal.userStatsTab.skillsListLabel": "Skill Categories:",
|
||||
"template.trackerEditorModal.userStatsTab.addSkillButton": "Add Skill Category",
|
||||
"template.trackerEditorModal.infoBoxTab.widgetsTitle": "Widgets",
|
||||
"template.trackerEditorModal.infoBoxTab.dateWidget": "Date",
|
||||
"template.trackerEditorModal.infoBoxTab.weatherWidget": "Weather",
|
||||
|
||||
@@ -22,6 +22,7 @@ import { renderInfoBox } from '../rendering/infoBox.js';
|
||||
import { renderThoughts } from '../rendering/thoughts.js';
|
||||
import { renderInventory } from '../rendering/inventory.js';
|
||||
import { renderQuests } from '../rendering/quests.js';
|
||||
import { renderSkills } from '../rendering/skills.js';
|
||||
import { i18n } from '../../core/i18n.js';
|
||||
|
||||
// Store the original preset name to restore after tracker generation
|
||||
@@ -145,7 +146,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
if (typeof renderSkills === 'function') renderSkills();
|
||||
renderSkills();
|
||||
saveChatData();
|
||||
} else {
|
||||
// JSON parsing failed - try legacy text-based parsing as fallback
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Supports both legacy text format and new JSON format
|
||||
*/
|
||||
|
||||
import { extensionSettings, FEATURE_FLAGS, addDebugLog } from '../../core/state.js';
|
||||
import { extensionSettings, FEATURE_FLAGS, addDebugLog, lastGeneratedData } from '../../core/state.js';
|
||||
import { saveSettings, saveChatData } from '../../core/persistence.js';
|
||||
import { extractInventory } from './inventoryParser.js';
|
||||
import { validateTrackerData, mergeTrackerData } from '../../types/trackerData.js';
|
||||
@@ -224,9 +224,22 @@ export function parseJSONTrackerData(jsonData) {
|
||||
debugLog('[RPG Parser] Mood:', jsonData.status.mood);
|
||||
}
|
||||
if (jsonData.status.fields) {
|
||||
// Store custom status fields
|
||||
extensionSettings.userStats.conditions = Object.values(jsonData.status.fields).join(', ') || 'None';
|
||||
debugLog('[RPG Parser] Status fields:', jsonData.status.fields);
|
||||
// Filter to only include configured status fields
|
||||
const configuredFields = extensionSettings.trackerConfig?.userStats?.statusSection?.customFields || [];
|
||||
const filteredValues = [];
|
||||
|
||||
for (const [key, value] of Object.entries(jsonData.status.fields)) {
|
||||
// Only include if this field is in the configured list (case-insensitive)
|
||||
const isConfigured = configuredFields.some(f =>
|
||||
f.toLowerCase() === key.toLowerCase()
|
||||
);
|
||||
if (isConfigured && value && value !== 'None' && value !== 'null') {
|
||||
filteredValues.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
extensionSettings.userStats.conditions = filteredValues.length > 0 ? filteredValues.join(', ') : 'None';
|
||||
debugLog('[RPG Parser] Status fields (filtered):', filteredValues);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,12 +262,65 @@ export function parseJSONTrackerData(jsonData) {
|
||||
infoBox.recentEvents = infoBox.recentEvents.filter(e => e && e !== 'null');
|
||||
extensionSettings.infoBoxData = infoBox;
|
||||
debugLog('[RPG Parser] InfoBox:', Object.keys(infoBox));
|
||||
|
||||
// Generate text format for lastGeneratedData.infoBox (needed for other UI parts)
|
||||
const textLines = [];
|
||||
if (infoBox.date) textLines.push(`Date: ${infoBox.date}`);
|
||||
if (infoBox.time) textLines.push(`Time: ${infoBox.time}`);
|
||||
if (infoBox.weather) textLines.push(`Weather: ${infoBox.weather}`);
|
||||
if (infoBox.temperature) textLines.push(`Temperature: ${infoBox.temperature}`);
|
||||
if (infoBox.location) textLines.push(`Location: ${infoBox.location}`);
|
||||
if (infoBox.recentEvents && infoBox.recentEvents.length > 0) {
|
||||
textLines.push(`Recent Events: ${infoBox.recentEvents.join(', ')}`);
|
||||
}
|
||||
if (textLines.length > 0) {
|
||||
lastGeneratedData.infoBox = textLines.join('\n');
|
||||
debugLog('[RPG Parser] Generated text format for infoBox');
|
||||
}
|
||||
}
|
||||
|
||||
// Parse characters - store for UI rendering
|
||||
// Parse characters - store for UI rendering AND generate text format for thought bubbles
|
||||
if (jsonData.characters && Array.isArray(jsonData.characters)) {
|
||||
extensionSettings.charactersData = jsonData.characters;
|
||||
debugLog('[RPG Parser] Characters:', jsonData.characters.length);
|
||||
|
||||
// Generate text format for lastGeneratedData.characterThoughts (needed for thought bubbles)
|
||||
const config = extensionSettings.trackerConfig?.presentCharacters;
|
||||
const thoughtsFieldName = config?.thoughts?.name || 'Thoughts';
|
||||
const lines = [];
|
||||
for (const char of jsonData.characters) {
|
||||
// Character name line
|
||||
lines.push(`- ${char.name || 'Unknown'}`);
|
||||
|
||||
// Details line with emoji and fields
|
||||
const details = [char.emoji || '😶'];
|
||||
const charFields = char.fields || {};
|
||||
for (const [key, value] of Object.entries(charFields)) {
|
||||
if (value) details.push(`${key}: ${value}`);
|
||||
}
|
||||
lines.push(`Details: ${details.join(' | ')}`);
|
||||
|
||||
// Relationship line
|
||||
if (char.relationship) {
|
||||
lines.push(`Relationship: ${char.relationship}`);
|
||||
}
|
||||
|
||||
// Stats line
|
||||
const charStats = char.stats || {};
|
||||
if (Object.keys(charStats).length > 0) {
|
||||
const statsStr = Object.entries(charStats).map(([k, v]) => `${k}: ${v}%`).join(' | ');
|
||||
lines.push(`Stats: ${statsStr}`);
|
||||
}
|
||||
|
||||
// Thoughts line
|
||||
if (char.thoughts) {
|
||||
lines.push(`${thoughtsFieldName}: ${char.thoughts}`);
|
||||
}
|
||||
}
|
||||
if (lines.length > 0) {
|
||||
lastGeneratedData.characterThoughts = lines.join('\n');
|
||||
debugLog('[RPG Parser] Generated text format for characterThoughts');
|
||||
}
|
||||
}
|
||||
|
||||
// Parse inventory (structured format)
|
||||
@@ -300,13 +366,33 @@ export function parseJSONTrackerData(jsonData) {
|
||||
|
||||
const previousItemNames = getItemNamesFromInventory(extensionSettings.inventoryV3);
|
||||
|
||||
// Get items - prefer 'items' for simplified mode, 'onPerson' for categorized
|
||||
// Also handle case where LLM uses either field
|
||||
const itemsArray = normalizeArray(jsonData.inventory.items);
|
||||
const onPersonArray = normalizeArray(jsonData.inventory.onPerson);
|
||||
|
||||
// For simplified mode: prefer 'items', fallback to 'onPerson'
|
||||
// For categorized mode: prefer 'onPerson', fallback to 'items'
|
||||
const simplifiedItems = itemsArray.length > 0 ? itemsArray : onPersonArray;
|
||||
const onPersonItems = onPersonArray.length > 0 ? onPersonArray : itemsArray;
|
||||
|
||||
extensionSettings.inventoryV3 = {
|
||||
onPerson: normalizeArray(jsonData.inventory.onPerson || jsonData.inventory.items),
|
||||
onPerson: onPersonItems,
|
||||
stored: jsonData.inventory.stored && typeof jsonData.inventory.stored === 'object'
|
||||
? jsonData.inventory.stored : {},
|
||||
assets: normalizeArray(jsonData.inventory.assets)
|
||||
assets: normalizeArray(jsonData.inventory.assets),
|
||||
// For simplified mode - use whichever array has items
|
||||
simplified: extensionSettings.useSimplifiedInventory ? simplifiedItems : (extensionSettings.inventoryV3?.simplified || [])
|
||||
};
|
||||
debugLog('[RPG Parser] Inventory parsed - onPerson:', extensionSettings.inventoryV3.onPerson.length);
|
||||
debugLog('[RPG Parser] Inventory parsed - onPerson:', extensionSettings.inventoryV3.onPerson.length,
|
||||
'simplified:', extensionSettings.inventoryV3.simplified?.length || 0);
|
||||
// Log first item to verify descriptions are preserved
|
||||
if (extensionSettings.inventoryV3.onPerson[0]) {
|
||||
debugLog('[RPG Parser] First onPerson item:', JSON.stringify(extensionSettings.inventoryV3.onPerson[0]));
|
||||
}
|
||||
if (extensionSettings.inventoryV3.simplified?.[0]) {
|
||||
debugLog('[RPG Parser] First simplified item:', JSON.stringify(extensionSettings.inventoryV3.simplified[0]));
|
||||
}
|
||||
|
||||
// Detect removed items and handle skill links
|
||||
const newItemNames = getItemNamesFromInventory(extensionSettings.inventoryV3);
|
||||
@@ -321,7 +407,7 @@ export function parseJSONTrackerData(jsonData) {
|
||||
// Also update legacy inventory for backwards compatibility
|
||||
const itemsToString = (items) => {
|
||||
if (!items || items.length === 0) return 'None';
|
||||
return items.map(i => i.name).join(', ');
|
||||
return items.map(i => typeof i === 'string' ? i : i.name).join(', ');
|
||||
};
|
||||
extensionSettings.userStats.inventory = {
|
||||
version: 2,
|
||||
@@ -329,7 +415,9 @@ export function parseJSONTrackerData(jsonData) {
|
||||
stored: Object.fromEntries(
|
||||
Object.entries(extensionSettings.inventoryV3.stored).map(([k, v]) => [k, itemsToString(v)])
|
||||
),
|
||||
assets: itemsToString(extensionSettings.inventoryV3.assets)
|
||||
assets: itemsToString(extensionSettings.inventoryV3.assets),
|
||||
// For simplified mode
|
||||
items: extensionSettings.useSimplifiedInventory ? itemsToString(extensionSettings.inventoryV3.simplified) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -347,6 +435,41 @@ export function parseJSONTrackerData(jsonData) {
|
||||
normalizedSkills[category] = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Validate grantedBy references - remove if item doesn't exist
|
||||
// Build set of all valid item names from current inventory (including just-parsed items)
|
||||
const validItemNames = new Set();
|
||||
const inv = extensionSettings.inventoryV3;
|
||||
if (inv) {
|
||||
const addItems = (items) => {
|
||||
if (!Array.isArray(items)) return;
|
||||
items.forEach(item => {
|
||||
const name = typeof item === 'string' ? item : item?.name;
|
||||
if (name) validItemNames.add(name.toLowerCase());
|
||||
});
|
||||
};
|
||||
addItems(inv.onPerson);
|
||||
addItems(inv.assets);
|
||||
addItems(inv.simplified);
|
||||
if (inv.stored) {
|
||||
Object.values(inv.stored).forEach(items => addItems(items));
|
||||
}
|
||||
}
|
||||
|
||||
// Check each skill's grantedBy and remove if invalid
|
||||
for (const abilities of Object.values(normalizedSkills)) {
|
||||
if (!Array.isArray(abilities)) continue;
|
||||
for (const ability of abilities) {
|
||||
if (ability && typeof ability === 'object' && ability.grantedBy) {
|
||||
const grantedByLower = ability.grantedBy.toLowerCase();
|
||||
if (!validItemNames.has(grantedByLower)) {
|
||||
debugLog('[RPG Parser] Removing invalid grantedBy:', ability.grantedBy, 'from skill:', ability.name);
|
||||
delete ability.grantedBy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extensionSettings.skillsV2 = normalizedSkills;
|
||||
debugLog('[RPG Parser] Skills parsed:', Object.keys(normalizedSkills));
|
||||
|
||||
@@ -356,6 +479,40 @@ export function parseJSONTrackerData(jsonData) {
|
||||
const names = abilities.map(a => typeof a === 'string' ? a : (a?.name || '')).filter(n => n);
|
||||
extensionSettings.skillsData[category] = names.join(', ') || 'None';
|
||||
}
|
||||
|
||||
// Validate grantsSkill references on items - remove if skill doesn't exist
|
||||
// Build set of all valid skill names from just-parsed skills
|
||||
const validSkillNames = new Set();
|
||||
for (const abilities of Object.values(normalizedSkills)) {
|
||||
if (!Array.isArray(abilities)) continue;
|
||||
abilities.forEach(ability => {
|
||||
const name = typeof ability === 'string' ? ability : ability?.name;
|
||||
if (name) validSkillNames.add(name.toLowerCase());
|
||||
});
|
||||
}
|
||||
|
||||
// Check each item's grantsSkill and remove if invalid
|
||||
const validateItemsGrantsSkill = (items) => {
|
||||
if (!Array.isArray(items)) return;
|
||||
for (const item of items) {
|
||||
if (item && typeof item === 'object' && item.grantsSkill) {
|
||||
const grantsSkillLower = item.grantsSkill.toLowerCase();
|
||||
if (!validSkillNames.has(grantsSkillLower)) {
|
||||
debugLog('[RPG Parser] Removing invalid grantsSkill:', item.grantsSkill, 'from item:', item.name);
|
||||
delete item.grantsSkill;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (inv) {
|
||||
validateItemsGrantsSkill(inv.onPerson);
|
||||
validateItemsGrantsSkill(inv.assets);
|
||||
validateItemsGrantsSkill(inv.simplified);
|
||||
if (inv.stored) {
|
||||
Object.values(inv.stored).forEach(items => validateItemsGrantsSkill(items));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse quests - handle different formats
|
||||
@@ -748,8 +905,11 @@ export function parseSkills(skillsText) {
|
||||
extensionSettings.skillAbilityLinks = {};
|
||||
}
|
||||
|
||||
// Get configured skill categories
|
||||
const configuredCategories = extensionSettings.trackerConfig?.userStats?.skillsSection?.customFields || [];
|
||||
// Get configured skill categories (handle both old string and new object format)
|
||||
const rawCategories = extensionSettings.trackerConfig?.userStats?.skillsSection?.customFields || [];
|
||||
const configuredCategories = rawCategories
|
||||
.filter(cat => typeof cat === 'string' || cat.enabled !== false)
|
||||
.map(cat => typeof cat === 'string' ? cat : cat.name);
|
||||
|
||||
const lines = skillsText.split('\n');
|
||||
const newSkillAbilityLinks = {};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { getContext } from '../../../../../../extensions.js';
|
||||
import { chat, getCurrentChatDetails, characters, this_chid } from '../../../../../../../script.js';
|
||||
import { selected_group, getGroupMembers, getGroupChat } from '../../../../../../group-chats.js';
|
||||
import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../core/state.js';
|
||||
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
||||
import { generateSchemaExample } from '../../types/trackerData.js';
|
||||
|
||||
// Type imports
|
||||
@@ -18,12 +18,6 @@ import { generateSchemaExample } from '../../types/trackerData.js';
|
||||
*/
|
||||
export const DEFAULT_HTML_PROMPT = `If appropriate, include inline HTML, CSS, and JS segments whenever they enhance visual storytelling (e.g., for in-world screens, posters, books, letters, signs, crests, labels, etc.). Style them to match the setting's theme (e.g., fantasy, sci-fi), keep the text readable, and embed all assets directly (using inline SVGs only with no external scripts, libraries, or fonts). Use these elements freely and naturally within the narrative as characters would encounter them, including animations, 3D effects, pop-ups, dropdowns, websites, and so on. Do not wrap the HTML/CSS/JS in code fences!`;
|
||||
|
||||
/**
|
||||
* Default tracker instruction prompt text (legacy text format)
|
||||
* Use {{user}} as placeholder for the user's name (will be replaced at runtime)
|
||||
*/
|
||||
export const DEFAULT_TRACKER_PROMPT = `At the start of every reply, you must attach an update to the trackers in EXACTLY the same format as below, enclosed in separate Markdown code fences. Replace X with actual numbers (e.g., 69) and replace all [placeholders] with concrete in-world details that {{user}} perceives about the current scene and the present characters. Do NOT keep the brackets or placeholder text in your response. For example: [Location] becomes Forest Clearing, [Mood Emoji] becomes 😊. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs).`;
|
||||
|
||||
/**
|
||||
* Default JSON tracker instruction prompt text
|
||||
* Use {{user}} as placeholder for the user's name (will be replaced at runtime)
|
||||
@@ -158,12 +152,12 @@ function buildAttributesString() {
|
||||
|
||||
// Get enabled attributes from config
|
||||
const rpgAttributes = userStatsConfig?.rpgAttributes || [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
];
|
||||
|
||||
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
|
||||
@@ -180,327 +174,6 @@ function buildAttributesString() {
|
||||
return attributeParts.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use generateJSONTrackerInstructions instead. This legacy text format
|
||||
* is kept for backwards compatibility with older LLM responses.
|
||||
*
|
||||
* Generates an example block showing current tracker states in markdown code blocks.
|
||||
* Uses COMMITTED data (not displayed data) for generation context.
|
||||
*
|
||||
* @returns {string} Formatted example text with tracker data in code blocks
|
||||
*/
|
||||
export function generateTrackerExample() {
|
||||
let example = '';
|
||||
|
||||
// Use COMMITTED data for generation context, not displayed data
|
||||
// Wrap each tracker section in markdown code blocks
|
||||
|
||||
// Build a combined stats/inventory/quests block if any are enabled
|
||||
let statsBlock = '';
|
||||
|
||||
if (extensionSettings.showUserStats && committedTrackerData.userStats) {
|
||||
statsBlock += committedTrackerData.userStats;
|
||||
}
|
||||
|
||||
// Add inventory example if enabled (and not already in userStats) - case-insensitive check
|
||||
if (extensionSettings.showInventory && extensionSettings.userStats?.inventory) {
|
||||
const inventorySummary = buildInventorySummary(extensionSettings.userStats.inventory);
|
||||
if (inventorySummary && inventorySummary !== 'None') {
|
||||
// Only add if not already present in userStats (case-insensitive)
|
||||
const statsBlockLower = statsBlock.toLowerCase();
|
||||
if (!statsBlockLower.includes('on person:') && !statsBlockLower.includes('inventory:')) {
|
||||
if (statsBlock) statsBlock += '\n';
|
||||
statsBlock += inventorySummary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add quests example if enabled - case-insensitive check
|
||||
if (extensionSettings.showQuests && extensionSettings.quests) {
|
||||
let questsText = '';
|
||||
if (extensionSettings.quests.main && extensionSettings.quests.main !== 'None') {
|
||||
questsText += `Main Quests: ${extensionSettings.quests.main}\n`;
|
||||
}
|
||||
if (extensionSettings.quests.optional && extensionSettings.quests.optional.length > 0) {
|
||||
const optionalQuests = extensionSettings.quests.optional.filter(q => q && q !== 'None').join(', ');
|
||||
if (optionalQuests) {
|
||||
questsText += `Optional Quests: ${optionalQuests}`;
|
||||
}
|
||||
}
|
||||
// Only add if not already present in userStats (case-insensitive)
|
||||
const statsBlockLower = statsBlock.toLowerCase();
|
||||
if (questsText && !statsBlockLower.includes('main quest') && !statsBlockLower.includes('optional quest')) {
|
||||
if (statsBlock) statsBlock += '\n';
|
||||
statsBlock += questsText;
|
||||
}
|
||||
}
|
||||
|
||||
if (statsBlock) {
|
||||
example += '```\n' + statsBlock.trim() + '\n```\n\n';
|
||||
}
|
||||
|
||||
if (extensionSettings.showInfoBox && committedTrackerData.infoBox) {
|
||||
example += '```\n' + committedTrackerData.infoBox + '\n```\n\n';
|
||||
}
|
||||
|
||||
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
|
||||
example += '```\n' + committedTrackerData.characterThoughts + '\n```';
|
||||
}
|
||||
|
||||
return example.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use generateJSONTrackerInstructions instead. This legacy text format
|
||||
* is kept for backwards compatibility with older LLM responses.
|
||||
*
|
||||
* Generates the instruction portion - format specifications and guidelines.
|
||||
*
|
||||
* @param {boolean} includeHtmlPrompt - Whether to include the HTML prompt (true for main generation, false for separate tracker generation)
|
||||
* @param {boolean} includeContinuation - Whether to include "After updating the trackers, continue..." instruction
|
||||
* @param {boolean} includeAttributes - Whether to include RPG attributes (false for separate tracker generation)
|
||||
* @returns {string} Formatted instruction text for the AI
|
||||
*/
|
||||
export function generateTrackerInstructions(includeHtmlPrompt = true, includeContinuation = true, includeAttributes = true) {
|
||||
const userName = getContext().name1;
|
||||
const classicStats = extensionSettings.classicStats;
|
||||
const trackerConfig = extensionSettings.trackerConfig;
|
||||
let instructions = '';
|
||||
|
||||
// Check if any trackers are enabled (including inventory, skills and quests as independent sections)
|
||||
const hasAnyTrackers = extensionSettings.showUserStats || extensionSettings.showInfoBox ||
|
||||
extensionSettings.showCharacterThoughts || extensionSettings.showSkills ||
|
||||
extensionSettings.showInventory || extensionSettings.showQuests;
|
||||
|
||||
// Only add tracker instructions if at least one tracker is enabled
|
||||
if (hasAnyTrackers) {
|
||||
// Universal instruction header - use custom prompt if set, otherwise use default
|
||||
const trackerPrompt = (extensionSettings.customTrackerPrompt || DEFAULT_TRACKER_PROMPT).replace(/\{\{user\}\}/g, userName);
|
||||
instructions += `\n${trackerPrompt}\n`;
|
||||
|
||||
// Check if we need a combined stats/inventory/quests code block
|
||||
const hasStatsBlock = extensionSettings.showUserStats || extensionSettings.showInventory || extensionSettings.showQuests;
|
||||
|
||||
if (hasStatsBlock) {
|
||||
const userStatsConfig = trackerConfig?.userStats;
|
||||
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||
|
||||
instructions += '```\n';
|
||||
|
||||
// Add user stats section if enabled
|
||||
if (extensionSettings.showUserStats) {
|
||||
instructions += `${userName}'s Stats\n`;
|
||||
instructions += '---\n';
|
||||
|
||||
// Add custom stats dynamically
|
||||
for (const stat of enabledStats) {
|
||||
instructions += `- ${stat.name}: X%\n`;
|
||||
}
|
||||
|
||||
// Add status section if enabled
|
||||
if (userStatsConfig?.statusSection?.enabled) {
|
||||
const statusFields = userStatsConfig.statusSection.customFields || [];
|
||||
const statusFieldsText = statusFields.map(f => `${f}`).join(', ');
|
||||
|
||||
if (userStatsConfig.statusSection.showMoodEmoji) {
|
||||
instructions += `Status: [Mood Emoji${statusFieldsText ? ', ' + statusFieldsText : ''}]\n`;
|
||||
} else if (statusFieldsText) {
|
||||
instructions += `Status: [${statusFieldsText}]\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add skills section if enabled in config AND NOT shown as separate section
|
||||
// When showSkills is true, skills are in their own tab and have their own code block
|
||||
if (userStatsConfig?.skillsSection?.enabled && !extensionSettings.showSkills) {
|
||||
const skillFields = userStatsConfig.skillsSection.customFields || [];
|
||||
const skillFieldsText = skillFields.map(f => `[${f}]`).join(', ');
|
||||
instructions += `Skills: [${skillFieldsText || 'Skill1, Skill2, etc.'}]\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add inventory format - independent of showUserStats
|
||||
if (extensionSettings.showInventory) {
|
||||
if (extensionSettings.useSimplifiedInventory) {
|
||||
// Simplified single-line inventory format
|
||||
instructions += 'Inventory: [Items currently carried/worn/owned, or "None"]\n';
|
||||
} else if (FEATURE_FLAGS.useNewInventory) {
|
||||
// Full v2 categorized inventory format
|
||||
instructions += 'On Person: [Items currently carried/worn, or "None"]\n';
|
||||
instructions += 'Stored - [Location Name]: [Items stored at this location]\n';
|
||||
instructions += '(Add multiple "Stored - [Location]:" lines as needed for different storage locations)\n';
|
||||
instructions += 'Assets: [Vehicles, property, major possessions, or "None"]\n';
|
||||
} else {
|
||||
// Legacy v1 format
|
||||
instructions += 'Inventory: [Clothing/Armor, Inventory Items (list of important items, or "None")]\\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Add quests section - independent of showUserStats
|
||||
if (extensionSettings.showQuests) {
|
||||
instructions += 'Main Quests: [Short title of the currently active main quest (for example, "Save the world"), or "None"]\n';
|
||||
instructions += 'Optional Quests: [Short titles of the currently active optional quests (for example, "Find Zandik\'s book"), or "None"]\n';
|
||||
}
|
||||
|
||||
instructions += '```\n\n';
|
||||
}
|
||||
|
||||
// Add separate skills section when showSkills is enabled
|
||||
if (extensionSettings.showSkills) {
|
||||
const skillsConfig = trackerConfig?.userStats?.skillsSection;
|
||||
const skillFields = skillsConfig?.customFields || [];
|
||||
|
||||
if (skillFields.length > 0) {
|
||||
instructions += '```\n';
|
||||
instructions += 'Skills\n';
|
||||
instructions += '---\n';
|
||||
|
||||
// Each skill category contains a list of abilities
|
||||
for (const skillName of skillFields) {
|
||||
if (extensionSettings.enableItemSkillLinks) {
|
||||
instructions += `${skillName}: [Abilities in this category, e.g. "Sword Fighting (Iron Sword), Parry" or "None"]\n`;
|
||||
} else {
|
||||
instructions += `${skillName}: [Abilities in this category, e.g. "Lockpicking, Sneaking" or "None"]\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionSettings.enableItemSkillLinks) {
|
||||
instructions += '\n(Abilities from items use parentheses: "Skill (Item)". Remove if item is removed or unequipped.)\n';
|
||||
}
|
||||
|
||||
instructions += '```\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionSettings.showInfoBox) {
|
||||
const infoBoxConfig = trackerConfig?.infoBox;
|
||||
const widgets = infoBoxConfig?.widgets || {};
|
||||
|
||||
instructions += '```\n';
|
||||
instructions += 'Info Box\n';
|
||||
instructions += '---\n';
|
||||
|
||||
// Add only enabled widgets
|
||||
if (widgets.date?.enabled) {
|
||||
instructions += 'Date: [Weekday, Month, Year]\n';
|
||||
}
|
||||
if (widgets.weather?.enabled) {
|
||||
instructions += 'Weather: [Weather Emoji, Forecast]\n';
|
||||
}
|
||||
if (widgets.temperature?.enabled) {
|
||||
const unit = widgets.temperature.unit === 'F' ? '°F' : '°C';
|
||||
instructions += `Temperature: [Temperature in ${unit}]\n`;
|
||||
}
|
||||
if (widgets.time?.enabled) {
|
||||
instructions += 'Time: [Time Start → Time End]\n';
|
||||
}
|
||||
if (widgets.location?.enabled) {
|
||||
instructions += 'Location: [Location]\n';
|
||||
}
|
||||
if (widgets.recentEvents?.enabled) {
|
||||
instructions += 'Recent Events: [Up to three past events leading to the ongoing scene (short descriptors with no details, for example, "last-night date with Mary")]\n';
|
||||
}
|
||||
|
||||
instructions += '```\n\n';
|
||||
}
|
||||
|
||||
if (extensionSettings.showCharacterThoughts) {
|
||||
const presentCharsConfig = trackerConfig?.presentCharacters;
|
||||
const enabledFields = presentCharsConfig?.customFields?.filter(f => f && f.enabled && f.name) || [];
|
||||
const relationshipFields = presentCharsConfig?.relationshipFields || [];
|
||||
const thoughtsConfig = presentCharsConfig?.thoughts;
|
||||
const characterStats = presentCharsConfig?.characterStats;
|
||||
const enabledCharStats = characterStats?.enabled && characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||
|
||||
instructions += '```\n';
|
||||
instructions += 'Present Characters\n';
|
||||
instructions += '---\n';
|
||||
|
||||
// Build relationship placeholders (e.g., "Lover/Friend")
|
||||
const relationshipPlaceholders = relationshipFields
|
||||
.filter(r => r && r.trim())
|
||||
.map(r => `${r}`)
|
||||
.join('/');
|
||||
|
||||
// Build custom field placeholders (e.g., "[Appearance] | [Current Action]")
|
||||
const fieldPlaceholders = enabledFields
|
||||
.map(f => `[${f.name}]`)
|
||||
.join(' | ');
|
||||
|
||||
// Character block format
|
||||
instructions += `- [Name (do not include ${userName}; state "Unavailable" if no major characters are present in the scene)]\n`;
|
||||
|
||||
// Details line with emoji and custom fields
|
||||
if (fieldPlaceholders) {
|
||||
instructions += `Details: [Present Character's Emoji] | ${fieldPlaceholders}\n`;
|
||||
} else {
|
||||
instructions += `Details: [Present Character's Emoji]\n`;
|
||||
}
|
||||
|
||||
// Relationship line (only if relationships are enabled)
|
||||
if (relationshipPlaceholders) {
|
||||
instructions += `Relationship: [(choose one: ${relationshipPlaceholders})]\n`;
|
||||
}
|
||||
|
||||
// Stats line (if enabled)
|
||||
if (enabledCharStats.length > 0) {
|
||||
const statPlaceholders = enabledCharStats.map(s => `${s.name}: X%`).join(' | ');
|
||||
instructions += `Stats: ${statPlaceholders}\n`;
|
||||
}
|
||||
|
||||
// Thoughts line (if enabled)
|
||||
if (thoughtsConfig?.enabled) {
|
||||
const thoughtsName = thoughtsConfig.name || 'Thoughts';
|
||||
const thoughtsDescription = thoughtsConfig.description || 'Internal monologue (in first person POV, up to three sentences long)';
|
||||
instructions += `${thoughtsName}: [${thoughtsDescription}]\n`;
|
||||
}
|
||||
|
||||
instructions += `- … (Repeat the format above for every other present major character)\n`;
|
||||
|
||||
instructions += '```\n\n';
|
||||
}
|
||||
|
||||
// Only add continuation instruction if includeContinuation is true
|
||||
if (includeContinuation) {
|
||||
instructions += `After updating the trackers, continue directly from where the last message in the chat history left off. Ensure the trackers you provide naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting the protagonist's performance, low hygiene influencing their social interactions, environmental factors shaping the scene, a character's emotional state coloring their responses, and so on. Remember, all bracketed placeholders (e.g., [Location], [Mood Emoji]) MUST be replaced with actual content without the square brackets.\n\n`;
|
||||
}
|
||||
|
||||
// Include attributes based on settings (only if includeAttributes is true)
|
||||
if (includeAttributes) {
|
||||
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
||||
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
|
||||
|
||||
if (shouldSendAttributes) {
|
||||
const attributesString = buildAttributesString();
|
||||
instructions += `${userName}'s attributes: ${attributesString}\n`;
|
||||
|
||||
// Add dice roll context if there was one
|
||||
if (extensionSettings.lastDiceRoll) {
|
||||
const roll = extensionSettings.lastDiceRoll;
|
||||
instructions += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
|
||||
} else {
|
||||
instructions += `\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append HTML prompt if enabled AND includeHtmlPrompt is true
|
||||
if (extensionSettings.enableHtmlPrompt && includeHtmlPrompt) {
|
||||
// Add newlines only if we had tracker instructions
|
||||
if (hasAnyTrackers) {
|
||||
instructions += ``;
|
||||
} else {
|
||||
instructions += `\n`;
|
||||
}
|
||||
|
||||
// Use custom HTML prompt if set, otherwise use default
|
||||
const htmlPrompt = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT;
|
||||
instructions += htmlPrompt;
|
||||
}
|
||||
|
||||
return instructions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates JSON-based tracker instructions.
|
||||
* Creates a prompt asking the LLM to output structured JSON data.
|
||||
@@ -581,7 +254,7 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
|
||||
infoParts.push(` "temperature": "22${unit}"`);
|
||||
}
|
||||
if (widgets.location?.enabled) infoParts.push(' "location": "Forest Clearing"');
|
||||
if (widgets.recentEvents?.enabled) infoParts.push(' "recentEvents": "Brief summary of recent events"');
|
||||
if (widgets.recentEvents?.enabled) infoParts.push(' "recentEvents": ["Event 1", "Event 2"]');
|
||||
|
||||
if (infoParts.length > 0) {
|
||||
sections.push(' "infoBox": {\n' + infoParts.join(',\n') + '\n }');
|
||||
@@ -591,10 +264,12 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
|
||||
// Characters section
|
||||
if (showCharacters) {
|
||||
const charConfig = trackerConfig?.presentCharacters || {};
|
||||
let charExample = ' {\n "name": "Character Name"';
|
||||
let charExample = ' {\n "name": "Character Name",\n "emoji": "🧑"';
|
||||
|
||||
if (charConfig.relationshipFields?.length > 0) {
|
||||
charExample += `,\n "relationship": "${charConfig.relationshipFields[0]}"`;
|
||||
// Show allowed relationship values as explanation
|
||||
const allowedRelationships = charConfig.relationshipFields.join(' | ');
|
||||
charExample += `,\n "relationship": "(${allowedRelationships})"`;
|
||||
}
|
||||
|
||||
const enabledFields = charConfig.customFields?.filter(f => f.enabled) || [];
|
||||
@@ -603,6 +278,14 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
|
||||
charExample += `,\n "fields": {\n${fieldsJson}\n }`;
|
||||
}
|
||||
|
||||
// Character stats (Health, Arousal, etc.)
|
||||
const charStatsConfig = charConfig.characterStats;
|
||||
const enabledCharStats = charStatsConfig?.enabled && charStatsConfig?.customStats?.filter(s => s?.enabled && s?.name) || [];
|
||||
if (enabledCharStats.length > 0) {
|
||||
const statsJson = enabledCharStats.map(s => ` "${s.name}": 75`).join(',\n');
|
||||
charExample += `,\n "stats": {\n${statsJson}\n }`;
|
||||
}
|
||||
|
||||
if (charConfig.thoughts?.enabled) {
|
||||
charExample += ',\n "thoughts": "Character\'s inner thoughts in first person..."';
|
||||
}
|
||||
@@ -640,14 +323,21 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
|
||||
// Skills section
|
||||
if (showSkills) {
|
||||
const skillCategories = trackerConfig?.userStats?.skillsSection?.customFields || [];
|
||||
if (skillCategories.length > 0) {
|
||||
// Filter to only enabled categories and handle both old (string) and new (object) formats
|
||||
const enabledCategories = skillCategories.filter(cat => {
|
||||
if (typeof cat === 'string') return true;
|
||||
return cat.enabled !== false;
|
||||
});
|
||||
|
||||
if (enabledCategories.length > 0) {
|
||||
let skillsSection = ' "skills": {\n';
|
||||
const categoryExamples = skillCategories.map(cat => {
|
||||
const categoryExamples = enabledCategories.map(cat => {
|
||||
const catName = typeof cat === 'string' ? cat : cat.name;
|
||||
let skillExample = '{ "name": "Ability Name", "description": "What this ability does" }';
|
||||
if (enableItemSkillLinks) {
|
||||
skillExample = '{ "name": "Ability", "description": "Description", "grantedBy": "Item Name" }';
|
||||
}
|
||||
return ` "${cat}": [${skillExample}]`;
|
||||
return ` "${catName}": [${skillExample}]`;
|
||||
});
|
||||
skillsSection += categoryExamples.join(',\n');
|
||||
skillsSection += '\n }';
|
||||
@@ -675,10 +365,41 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
|
||||
instructions += '- Empty arrays [] for sections with no items\n';
|
||||
instructions += '- null for main quest if none active\n';
|
||||
|
||||
// Add stat descriptions if any have descriptions
|
||||
if (showStats) {
|
||||
const customStats = trackerConfig?.userStats?.customStats || [];
|
||||
const statsWithDesc = customStats.filter(s => s?.enabled && s?.description);
|
||||
if (statsWithDesc.length > 0) {
|
||||
instructions += '- Stat meanings:\n';
|
||||
statsWithDesc.forEach(stat => {
|
||||
instructions += ` • "${stat.name}": ${stat.description}\n`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (showSkills) {
|
||||
const skillsLabel = trackerConfig?.userStats?.skillsSection?.label || 'Skills';
|
||||
if (skillsLabel !== 'Skills') {
|
||||
instructions += `- The "skills" section represents "${skillsLabel}" in this context\n`;
|
||||
}
|
||||
|
||||
// Add skill category descriptions if any have descriptions
|
||||
const skillCategories = trackerConfig?.userStats?.skillsSection?.customFields || [];
|
||||
const categoriesWithDesc = skillCategories.filter(cat =>
|
||||
typeof cat === 'object' && cat.description && cat.enabled !== false
|
||||
);
|
||||
if (categoriesWithDesc.length > 0) {
|
||||
instructions += `- ${skillsLabel} categories:\n`;
|
||||
categoriesWithDesc.forEach(cat => {
|
||||
instructions += ` • "${cat.name}": ${cat.description}\n`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (enableItemSkillLinks) {
|
||||
instructions += '- Items can grant skills: add "grantsSkill": "Skill Name" to the item\n';
|
||||
instructions += '- Skills from items: add "grantedBy": "Item Name" to the skill\n';
|
||||
instructions += '- If an item is removed/lost, remove its linked skill too\n';
|
||||
instructions += '- Items can grant skills: add {"grantsSkill": "Skill Name"} to the item object\n';
|
||||
instructions += '- When a skill comes from an item, add {"grantedBy": "Item Name"} to that skill object\n';
|
||||
instructions += '- If an item is removed/lost, also remove any skill it granted\n';
|
||||
}
|
||||
|
||||
instructions += '\n';
|
||||
@@ -696,6 +417,16 @@ export function generateJSONTrackerInstructions(includeHtmlPrompt = true, includ
|
||||
if (shouldSendAttributes) {
|
||||
const attributesString = buildAttributesString();
|
||||
instructions += `${userName}'s attributes: ${attributesString}\n`;
|
||||
|
||||
// Add attribute descriptions if any have descriptions
|
||||
const rpgAttributes = trackerConfig?.userStats?.rpgAttributes || [];
|
||||
const attrsWithDesc = rpgAttributes.filter(a => a?.enabled && a?.description);
|
||||
if (attrsWithDesc.length > 0) {
|
||||
instructions += 'Attribute meanings:\n';
|
||||
attrsWithDesc.forEach(attr => {
|
||||
instructions += ` • ${attr.name}: ${attr.description}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
if (extensionSettings.lastDiceRoll) {
|
||||
const roll = extensionSettings.lastDiceRoll;
|
||||
@@ -862,17 +593,38 @@ export function generateRPGPromptText() {
|
||||
previousState.infoBox = extensionSettings.infoBoxData;
|
||||
}
|
||||
|
||||
// Characters
|
||||
// Characters - format to match schema
|
||||
if (extensionSettings.showCharacterThoughts && extensionSettings.charactersData?.length > 0) {
|
||||
previousState.characters = extensionSettings.charactersData;
|
||||
// Ensure characters match the expected schema format
|
||||
previousState.characters = extensionSettings.charactersData.map(char => {
|
||||
const formatted = { name: char.name };
|
||||
if (char.relationship) formatted.relationship = char.relationship;
|
||||
if (char.emoji) formatted.emoji = char.emoji;
|
||||
if (char.fields && Object.keys(char.fields).length > 0) formatted.fields = char.fields;
|
||||
if (char.stats && Object.keys(char.stats).length > 0) formatted.stats = char.stats;
|
||||
if (char.thoughts) formatted.thoughts = char.thoughts;
|
||||
return formatted;
|
||||
});
|
||||
}
|
||||
|
||||
// Inventory
|
||||
if (extensionSettings.showInventory) {
|
||||
if (extensionSettings.inventoryV3 && (extensionSettings.inventoryV3.onPerson?.length > 0 ||
|
||||
Object.keys(extensionSettings.inventoryV3.stored || {}).length > 0 ||
|
||||
extensionSettings.inventoryV3.assets?.length > 0)) {
|
||||
previousState.inventory = extensionSettings.inventoryV3;
|
||||
// Inventory - format to match schema (use "items" for simplified mode)
|
||||
if (extensionSettings.showInventory && extensionSettings.inventoryV3) {
|
||||
const inv = extensionSettings.inventoryV3;
|
||||
if (extensionSettings.useSimplifiedInventory) {
|
||||
// Simplified mode uses "items" key
|
||||
const items = inv.simplified || inv.onPerson || [];
|
||||
if (items.length > 0) {
|
||||
previousState.inventory = { items };
|
||||
}
|
||||
} else {
|
||||
// Full categorized mode
|
||||
if (inv.onPerson?.length > 0 || Object.keys(inv.stored || {}).length > 0 || inv.assets?.length > 0) {
|
||||
previousState.inventory = {
|
||||
onPerson: inv.onPerson || [],
|
||||
stored: inv.stored || {},
|
||||
assets: inv.assets || []
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ export async function onMessageReceived(data) {
|
||||
renderThoughts();
|
||||
renderInventory();
|
||||
renderQuests();
|
||||
if (typeof renderSkills === 'function') renderSkills();
|
||||
renderSkills();
|
||||
saveChatData();
|
||||
return; // Skip legacy text parsing
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ export function removeItem(field, itemIndex, location) {
|
||||
if (!removedItemName) {
|
||||
removedItemName = items[itemIndex];
|
||||
}
|
||||
|
||||
|
||||
items.splice(itemIndex, 1); // Remove item at index
|
||||
|
||||
// Check if this item was linked to a skill and handle removal
|
||||
|
||||
@@ -74,152 +74,6 @@ function hasStructuredInfoBoxData(data) {
|
||||
isValidValue(data.time) || isValidValue(data.location) || hasEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the info box using structured JSON data
|
||||
* @param {Object} data - Structured infoBox data
|
||||
*/
|
||||
function renderStructuredInfoBox(data) {
|
||||
const config = extensionSettings.trackerConfig?.infoBox;
|
||||
const widgets = config?.widgets || {};
|
||||
|
||||
// Build widgets HTML
|
||||
let widgetsHtml = '';
|
||||
let widgetCount = 0;
|
||||
|
||||
// Date widget - skip null values
|
||||
if (widgets.date?.enabled && isValidValue(data.date)) {
|
||||
widgetCount++;
|
||||
widgetsHtml += `
|
||||
<div class="rpg-dashboard-widget rpg-calendar-widget">
|
||||
<i class="fa-solid fa-calendar"></i>
|
||||
<div class="rpg-widget-content">
|
||||
<span class="rpg-widget-label" data-i18n-key="infobox.date">${i18n.getTranslation('infobox.date')}</span>
|
||||
<span class="rpg-widget-value rpg-editable" contenteditable="true" data-field="date">${data.date}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Weather widget - skip null values
|
||||
if (widgets.weather?.enabled && isValidValue(data.weather)) {
|
||||
widgetCount++;
|
||||
const { emoji, text } = separateEmojiFromText(data.weather);
|
||||
widgetsHtml += `
|
||||
<div class="rpg-dashboard-widget rpg-weather-widget">
|
||||
<span class="rpg-weather-emoji rpg-editable" contenteditable="true" data-field="weatherEmoji">${emoji || '🌤️'}</span>
|
||||
<div class="rpg-widget-content">
|
||||
<span class="rpg-widget-label" data-i18n-key="infobox.weather">${i18n.getTranslation('infobox.weather')}</span>
|
||||
<span class="rpg-widget-value rpg-editable" contenteditable="true" data-field="weather">${text}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Temperature widget - skip null values
|
||||
if (widgets.temperature?.enabled && isValidValue(data.temperature)) {
|
||||
widgetCount++;
|
||||
widgetsHtml += `
|
||||
<div class="rpg-dashboard-widget rpg-temperature-widget">
|
||||
<i class="fa-solid fa-temperature-half"></i>
|
||||
<div class="rpg-widget-content">
|
||||
<span class="rpg-widget-label" data-i18n-key="infobox.temperature">${i18n.getTranslation('infobox.temperature')}</span>
|
||||
<span class="rpg-widget-value rpg-editable" contenteditable="true" data-field="temperature">${data.temperature}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Time widget - skip null values
|
||||
if (widgets.time?.enabled && isValidValue(data.time)) {
|
||||
widgetCount++;
|
||||
widgetsHtml += `
|
||||
<div class="rpg-dashboard-widget rpg-time-widget">
|
||||
<i class="fa-solid fa-clock"></i>
|
||||
<div class="rpg-widget-content">
|
||||
<span class="rpg-widget-label" data-i18n-key="infobox.time">${i18n.getTranslation('infobox.time')}</span>
|
||||
<span class="rpg-widget-value rpg-editable" contenteditable="true" data-field="time">${data.time}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Location widget - skip null values
|
||||
if (widgets.location?.enabled && isValidValue(data.location)) {
|
||||
widgetCount++;
|
||||
widgetsHtml += `
|
||||
<div class="rpg-dashboard-widget rpg-location-widget">
|
||||
<i class="fa-solid fa-map-marker-alt"></i>
|
||||
<div class="rpg-widget-content">
|
||||
<span class="rpg-widget-label" data-i18n-key="infobox.location">${i18n.getTranslation('infobox.location')}</span>
|
||||
<span class="rpg-widget-value rpg-editable" contenteditable="true" data-field="location">${data.location}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Recent events widget - handle both string and array formats
|
||||
const recentEvents = Array.isArray(data.recentEvents)
|
||||
? data.recentEvents
|
||||
: (data.recentEvents ? [data.recentEvents] : []);
|
||||
if (widgets.recentEvents?.enabled && recentEvents.length > 0) {
|
||||
widgetCount++;
|
||||
const eventsHtml = recentEvents.map((event, i) =>
|
||||
`<li class="rpg-event-item rpg-editable" contenteditable="true" data-field="recentEvents" data-index="${i}">${event}</li>`
|
||||
).join('');
|
||||
widgetsHtml += `
|
||||
<div class="rpg-dashboard-widget rpg-events-widget rpg-widget-wide">
|
||||
<i class="fa-solid fa-scroll"></i>
|
||||
<div class="rpg-widget-content">
|
||||
<span class="rpg-widget-label" data-i18n-key="infobox.recentEvents">${i18n.getTranslation('infobox.recentEvents')}</span>
|
||||
<ul class="rpg-events-list">${eventsHtml}</ul>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Determine layout class based on widget count
|
||||
const layoutClass = widgetCount <= 2 ? 'rpg-dashboard-row-1' :
|
||||
widgetCount <= 4 ? 'rpg-dashboard-row-2' : 'rpg-dashboard-row-3';
|
||||
|
||||
const html = `<div class="rpg-dashboard ${layoutClass}">${widgetsHtml}</div>`;
|
||||
|
||||
$infoBoxContainer.html(html);
|
||||
|
||||
// Remove updating animation
|
||||
if (extensionSettings.enableAnimations) {
|
||||
setTimeout(() => $infoBoxContainer.removeClass('rpg-content-updating'), 300);
|
||||
}
|
||||
|
||||
// Setup event listeners for editable fields
|
||||
setupStructuredInfoBoxEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for structured info box editing
|
||||
*/
|
||||
function setupStructuredInfoBoxEventListeners() {
|
||||
$infoBoxContainer.off('blur', '.rpg-editable').on('blur', '.rpg-editable', function() {
|
||||
const $this = $(this);
|
||||
const field = $this.data('field');
|
||||
const index = $this.data('index');
|
||||
const newValue = $this.text().trim();
|
||||
|
||||
if (!extensionSettings.infoBoxData) {
|
||||
extensionSettings.infoBoxData = {};
|
||||
}
|
||||
|
||||
if (field === 'recentEvents' && index !== undefined) {
|
||||
if (!extensionSettings.infoBoxData.recentEvents) {
|
||||
extensionSettings.infoBoxData.recentEvents = [];
|
||||
}
|
||||
extensionSettings.infoBoxData.recentEvents[index] = newValue;
|
||||
} else if (field === 'weatherEmoji') {
|
||||
// Combine emoji with existing weather text
|
||||
const currentWeather = extensionSettings.infoBoxData.weather || '';
|
||||
const { text } = separateEmojiFromText(currentWeather);
|
||||
extensionSettings.infoBoxData.weather = newValue + ' ' + text;
|
||||
} else {
|
||||
extensionSettings.infoBoxData[field] = newValue;
|
||||
}
|
||||
|
||||
saveChatData();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the info box as a visual dashboard with calendar, weather, temperature, clock, and map widgets.
|
||||
* Includes event listeners for editable fields.
|
||||
@@ -651,9 +505,21 @@ export function renderInfoBox() {
|
||||
|
||||
// Row 3: Recent Events widget (notebook style) - show if enabled
|
||||
if (config?.widgets?.recentEvents?.enabled) {
|
||||
// Parse Recent Events from infoBox string
|
||||
// Get Recent Events from structured data (JSON) or text format
|
||||
let recentEvents = [];
|
||||
if (committedTrackerData.infoBox) {
|
||||
|
||||
// First check structured infoBoxData (from JSON parsing)
|
||||
if (extensionSettings.infoBoxData?.recentEvents) {
|
||||
const events = extensionSettings.infoBoxData.recentEvents;
|
||||
if (Array.isArray(events)) {
|
||||
recentEvents = events.filter(e => e && e !== 'null');
|
||||
} else if (typeof events === 'string' && events !== 'null') {
|
||||
recentEvents = [events];
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to text format from committedTrackerData
|
||||
if (recentEvents.length === 0 && committedTrackerData.infoBox) {
|
||||
const recentEventsLine = committedTrackerData.infoBox.split('\n').find(line => line.startsWith('Recent Events:'));
|
||||
if (recentEventsLine) {
|
||||
const eventsString = recentEventsLine.replace('Recent Events:', '').trim();
|
||||
|
||||
@@ -64,7 +64,7 @@ export function renderInventorySubTabs(activeTab = 'onPerson') {
|
||||
|
||||
/**
|
||||
* Gets the description for an item from structured inventory data
|
||||
* @param {string} field - Field type ('onPerson', 'stored', 'assets')
|
||||
* @param {string} field - Field type ('onPerson', 'stored', 'assets', 'simplified')
|
||||
* @param {number} index - Item index
|
||||
* @param {string} [location] - Location name for stored items
|
||||
* @returns {string} Item description or empty string
|
||||
@@ -80,6 +80,8 @@ function getItemDescription(field, index, location = null) {
|
||||
items = inv3.assets;
|
||||
} else if (field === 'stored' && location) {
|
||||
items = inv3.stored?.[location];
|
||||
} else if (field === 'simplified') {
|
||||
items = inv3.simplified;
|
||||
}
|
||||
|
||||
if (!items || !Array.isArray(items) || !items[index]) return '';
|
||||
@@ -544,27 +546,39 @@ export function renderSimplifiedInventoryView(itemsString, viewMode = 'list') {
|
||||
itemsHtml = `<div class="rpg-inventory-empty" data-i18n-key="inventory.simplified.empty">${i18n.getTranslation('inventory.simplified.empty')}</div>`;
|
||||
} else {
|
||||
if (viewMode === 'grid') {
|
||||
// Grid view: card-style items
|
||||
itemsHtml = items.map((item, index) => `
|
||||
// Grid view: card-style items (same as onPerson)
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const desc = getItemDescription('simplified', index);
|
||||
return `
|
||||
<div class="rpg-item-card ${itemHasLinkedSkills(item) ? 'rpg-has-skill-link' : ''}" data-field="simplified" data-index="${index}">
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="simplified" data-index="${index}" title="${i18n.getTranslation('inventory.simplified.removeTitle')}">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
${getSkillLinkIndicator(item)}
|
||||
<div class="rpg-item-desc-row">
|
||||
<span class="rpg-item-description rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" data-prop="description" title="Click to edit description">${escapeHtml(desc)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
} else {
|
||||
// List view: full-width rows
|
||||
itemsHtml = items.map((item, index) => `
|
||||
// List view: full-width rows (same as onPerson)
|
||||
itemsHtml = items.map((item, index) => {
|
||||
const desc = getItemDescription('simplified', index);
|
||||
return `
|
||||
<div class="rpg-item-row ${itemHasLinkedSkills(item) ? 'rpg-has-skill-link' : ''}" data-field="simplified" data-index="${index}">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
${getSkillLinkIndicator(item)}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="simplified" data-index="${index}" title="${i18n.getTranslation('inventory.simplified.removeTitle')}">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<div class="rpg-item-main-row">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
||||
${getSkillLinkIndicator(item)}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="simplified" data-index="${index}" title="${i18n.getTranslation('inventory.simplified.removeTitle')}">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="rpg-item-desc-row">
|
||||
<span class="rpg-item-description rpg-editable" contenteditable="true" data-field="simplified" data-index="${index}" data-prop="description" title="Click to edit description">${escapeHtml(desc)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`}).join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,169 +624,6 @@ export function renderSimplifiedInventoryView(itemsString, viewMode = 'list') {
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a single structured item (with name + description)
|
||||
* @param {Object} item - Item object with name, description, grantsSkill
|
||||
* @param {string} field - Field type ('onPerson', 'stored', 'assets')
|
||||
* @param {number} index - Item index
|
||||
* @param {string} viewMode - 'list' or 'grid'
|
||||
* @param {string} [location] - Location name for stored items
|
||||
* @returns {string} HTML for the item
|
||||
*/
|
||||
function renderStructuredItem(item, field, index, viewMode, location = null) {
|
||||
// Normalize item - handle both string and object formats
|
||||
const normalizedItem = typeof item === 'string'
|
||||
? { name: item, description: '' }
|
||||
: { name: item?.name || 'Unknown', description: item?.description || '', grantsSkill: item?.grantsSkill };
|
||||
|
||||
const hasSkillLink = normalizedItem.grantsSkill || itemHasLinkedSkills(normalizedItem.name);
|
||||
const skillLinkHtml = hasSkillLink ? getSkillLinkIndicator(normalizedItem.name) : '';
|
||||
const grantsBadge = normalizedItem.grantsSkill
|
||||
? `<span class="rpg-item-grants-badge" title="Grants: ${escapeHtml(normalizedItem.grantsSkill)}"><i class="fa-solid fa-star"></i></span>`
|
||||
: '';
|
||||
|
||||
const locationAttr = location ? `data-location="${escapeHtml(location)}"` : '';
|
||||
|
||||
if (viewMode === 'grid') {
|
||||
return `
|
||||
<div class="rpg-item-card ${hasSkillLink ? 'rpg-has-skill-link' : ''}" data-field="${field}" data-index="${index}" ${locationAttr}>
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="${field}" data-index="${index}" ${locationAttr} title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
<div class="rpg-item-content">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="${field}" data-index="${index}" ${locationAttr} title="Click to edit name">${escapeHtml(normalizedItem.name)}</span>
|
||||
${grantsBadge}
|
||||
<span class="rpg-item-description rpg-editable" contenteditable="true" data-field="${field}" data-index="${index}" data-prop="description" ${locationAttr} title="Click to edit description">${escapeHtml(normalizedItem.description)}</span>
|
||||
</div>
|
||||
${skillLinkHtml}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
<div class="rpg-item-row ${hasSkillLink ? 'rpg-has-skill-link' : ''}" data-field="${field}" data-index="${index}" ${locationAttr}>
|
||||
<div class="rpg-item-info">
|
||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="${field}" data-index="${index}" ${locationAttr} title="Click to edit name">${escapeHtml(normalizedItem.name)}</span>
|
||||
${grantsBadge}
|
||||
<span class="rpg-item-description rpg-editable" contenteditable="true" data-field="${field}" data-index="${index}" data-prop="description" ${locationAttr} title="Click to edit description">${escapeHtml(normalizedItem.description)}</span>
|
||||
</div>
|
||||
${skillLinkHtml}
|
||||
<button class="rpg-item-remove" data-action="remove-item" data-field="${field}" data-index="${index}" ${locationAttr} title="Remove item">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders structured inventory (v3 format with name + description)
|
||||
* @param {Object} inventoryV3 - Structured inventory data
|
||||
* @param {Object} options - Render options
|
||||
* @returns {string} HTML
|
||||
*/
|
||||
function renderStructuredInventory(inventoryV3, options) {
|
||||
const { activeTab = 'onPerson' } = options;
|
||||
const viewModes = extensionSettings.inventoryViewModes || {};
|
||||
|
||||
// Sub-tabs
|
||||
let html = renderInventorySubTabs(activeTab);
|
||||
|
||||
// On Person tab
|
||||
const onPersonMode = viewModes.onPerson || 'list';
|
||||
const onPersonItems = inventoryV3.onPerson || [];
|
||||
let onPersonHtml = onPersonItems.length === 0
|
||||
? `<div class="rpg-inventory-empty">${i18n.getTranslation('inventory.onPerson.empty')}</div>`
|
||||
: onPersonItems.map((item, i) => renderStructuredItem(item, 'onPerson', i, onPersonMode)).join('');
|
||||
|
||||
html += `
|
||||
<div class="rpg-inventory-tab-content ${activeTab === 'onPerson' ? 'active' : ''}" data-tab="onPerson">
|
||||
<div class="rpg-inventory-header">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${onPersonMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="onPerson" data-view="list" title="${i18n.getTranslation('global.listView')}">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</button>
|
||||
<button class="rpg-view-btn ${onPersonMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="onPerson" data-view="grid" title="${i18n.getTranslation('global.gridView')}">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="onPerson" title="Add new item">
|
||||
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('inventory.onPerson.addItemButton')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="rpg-item-list rpg-item-${onPersonMode}-view">${onPersonHtml}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Stored tab
|
||||
const storedMode = viewModes.stored || 'list';
|
||||
const stored = inventoryV3.stored || {};
|
||||
let storedHtml = '';
|
||||
|
||||
for (const [location, items] of Object.entries(stored)) {
|
||||
const locationItems = items.map((item, i) => renderStructuredItem(item, 'stored', i, storedMode, location)).join('');
|
||||
storedHtml += `
|
||||
<div class="rpg-storage-location" data-location="${escapeHtml(location)}">
|
||||
<div class="rpg-location-header">
|
||||
<span class="rpg-location-name">${escapeHtml(location)}</span>
|
||||
<span class="rpg-location-count">(${items.length})</span>
|
||||
</div>
|
||||
<div class="rpg-item-list rpg-item-${storedMode}-view">${locationItems}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (Object.keys(stored).length === 0) {
|
||||
storedHtml = `<div class="rpg-inventory-empty">${i18n.getTranslation('inventory.stored.empty')}</div>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="rpg-inventory-tab-content ${activeTab === 'stored' ? 'active' : ''}" data-tab="stored">
|
||||
<div class="rpg-inventory-header">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${storedMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="stored" data-view="list" title="${i18n.getTranslation('global.listView')}">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</button>
|
||||
<button class="rpg-view-btn ${storedMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="stored" data-view="grid" title="${i18n.getTranslation('global.gridView')}">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-location" title="Add storage location">
|
||||
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('inventory.stored.addLocationButton')}
|
||||
</button>
|
||||
</div>
|
||||
${storedHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Assets tab
|
||||
const assetsMode = viewModes.assets || 'list';
|
||||
const assets = inventoryV3.assets || [];
|
||||
let assetsHtml = assets.length === 0
|
||||
? `<div class="rpg-inventory-empty">${i18n.getTranslation('inventory.assets.empty')}</div>`
|
||||
: assets.map((item, i) => renderStructuredItem(item, 'assets', i, assetsMode)).join('');
|
||||
|
||||
html += `
|
||||
<div class="rpg-inventory-tab-content ${activeTab === 'assets' ? 'active' : ''}" data-tab="assets">
|
||||
<div class="rpg-inventory-header">
|
||||
<div class="rpg-view-toggle">
|
||||
<button class="rpg-view-btn ${assetsMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="assets" data-view="list" title="${i18n.getTranslation('global.listView')}">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</button>
|
||||
<button class="rpg-view-btn ${assetsMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="assets" data-view="grid" title="${i18n.getTranslation('global.gridView')}">
|
||||
<i class="fa-solid fa-th"></i>
|
||||
</button>
|
||||
</div>
|
||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="assets" title="Add new asset">
|
||||
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('inventory.assets.addAssetButton')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="rpg-item-list rpg-item-${assetsMode}-view">${assetsHtml}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `<div class="rpg-inventory-container rpg-structured">${html}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we have structured inventory data (v3 format)
|
||||
* @returns {boolean}
|
||||
@@ -782,7 +633,8 @@ function hasStructuredInventory() {
|
||||
return inv && (
|
||||
(inv.onPerson && inv.onPerson.length > 0) ||
|
||||
(inv.assets && inv.assets.length > 0) ||
|
||||
(inv.stored && Object.keys(inv.stored).length > 0)
|
||||
(inv.stored && Object.keys(inv.stored).length > 0) ||
|
||||
(inv.simplified && inv.simplified.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -815,7 +667,9 @@ export function renderInventory() {
|
||||
stored: Object.fromEntries(
|
||||
Object.entries(inv.stored || {}).map(([k, v]) => [k, itemsToString(v)])
|
||||
),
|
||||
assets: itemsToString(inv.assets)
|
||||
assets: itemsToString(inv.assets),
|
||||
// For simplified mode
|
||||
items: itemsToString(inv.simplified)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,11 @@ function serializeItems(items) {
|
||||
* @returns {string[]} Array of skill category names
|
||||
*/
|
||||
export function getSkillCategories() {
|
||||
return extensionSettings.trackerConfig?.userStats?.skillsSection?.customFields || [];
|
||||
const categories = extensionSettings.trackerConfig?.userStats?.skillsSection?.customFields || [];
|
||||
// Handle both old format (string array) and new format (object array)
|
||||
return categories
|
||||
.filter(cat => typeof cat === 'string' || cat.enabled !== false)
|
||||
.map(cat => typeof cat === 'string' ? cat : cat.name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,30 +315,93 @@ export function unlinkAbility(skillName, abilityName) {
|
||||
|
||||
/**
|
||||
* Gets all skill abilities linked to a specific inventory item
|
||||
* Checks both manual skillAbilityLinks and structured skillsV2 with grantedBy
|
||||
* @param {string} itemName - The inventory item name
|
||||
* @returns {Array<{skillName: string, abilityName: string}>} Array of linked abilities
|
||||
*/
|
||||
export function getAbilitiesLinkedToItem(itemName) {
|
||||
if (!extensionSettings.skillAbilityLinks || !itemName) return [];
|
||||
if (!itemName) return [];
|
||||
const linked = [];
|
||||
const normalizedItemName = itemName.toLowerCase().trim();
|
||||
for (const [key, linkedItem] of Object.entries(extensionSettings.skillAbilityLinks)) {
|
||||
// Case-insensitive comparison
|
||||
if (linkedItem && linkedItem.toLowerCase().trim() === normalizedItemName) {
|
||||
const [skillName, abilityName] = key.split('::');
|
||||
linked.push({ skillName, abilityName });
|
||||
|
||||
// Check manual skillAbilityLinks
|
||||
if (extensionSettings.skillAbilityLinks) {
|
||||
for (const [key, linkedItem] of Object.entries(extensionSettings.skillAbilityLinks)) {
|
||||
// Case-insensitive comparison
|
||||
if (linkedItem && linkedItem.toLowerCase().trim() === normalizedItemName) {
|
||||
const [skillName, abilityName] = key.split('::');
|
||||
linked.push({ skillName, abilityName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check structured skillsV2 for abilities with grantedBy matching this item
|
||||
const skillsV2 = extensionSettings.skillsV2;
|
||||
if (skillsV2 && typeof skillsV2 === 'object') {
|
||||
for (const [skillName, abilities] of Object.entries(skillsV2)) {
|
||||
if (!Array.isArray(abilities)) continue;
|
||||
for (const ability of abilities) {
|
||||
if (!ability || typeof ability !== 'object') continue;
|
||||
const grantedBy = (ability.grantedBy || '').toLowerCase().trim();
|
||||
if (grantedBy === normalizedItemName) {
|
||||
// Avoid duplicates
|
||||
const exists = linked.some(l => l.skillName === skillName && l.abilityName === ability.name);
|
||||
if (!exists) {
|
||||
linked.push({ skillName, abilityName: ability.name });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return linked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an inventory item has any linked skills
|
||||
* Checks both manual skillAbilityLinks and structured grantsSkill property
|
||||
* @param {string} itemName - The inventory item name
|
||||
* @returns {boolean} True if item has linked skills
|
||||
*/
|
||||
export function itemHasLinkedSkills(itemName) {
|
||||
return getAbilitiesLinkedToItem(itemName).length > 0;
|
||||
// Check manual links first
|
||||
if (getAbilitiesLinkedToItem(itemName).length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check structured inventory for grantsSkill property
|
||||
const inv = extensionSettings.inventoryV3;
|
||||
if (!inv || !itemName) return false;
|
||||
|
||||
const normalizedName = itemName.toLowerCase().trim();
|
||||
|
||||
// Helper to check if an item array contains the item with grantsSkill
|
||||
const checkItems = (items) => {
|
||||
if (!Array.isArray(items)) return false;
|
||||
return items.some(item => {
|
||||
if (!item || typeof item !== 'object') return false;
|
||||
const name = (item.name || '').toLowerCase().trim();
|
||||
return name === normalizedName && item.grantsSkill;
|
||||
});
|
||||
};
|
||||
|
||||
// Check onPerson
|
||||
if (checkItems(inv.onPerson)) return true;
|
||||
|
||||
// Check simplified
|
||||
if (checkItems(inv.simplified)) return true;
|
||||
|
||||
// Check assets
|
||||
if (checkItems(inv.assets)) return true;
|
||||
|
||||
// Check stored locations
|
||||
if (inv.stored && typeof inv.stored === 'object') {
|
||||
for (const items of Object.values(inv.stored)) {
|
||||
if (checkItems(items)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -101,196 +101,6 @@ function namesMatch(cardName, aiName) {
|
||||
return wordBoundary.test(aiCore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders character thoughts (Present Characters) panel.
|
||||
* Displays character cards with avatars, relationship badges, and traits.
|
||||
* Includes event listeners for editable character fields.
|
||||
*/
|
||||
/**
|
||||
* Converts structured character data to the internal format used by the renderer
|
||||
* @param {Array} charactersData - Array of structured character objects
|
||||
* @param {Object} config - Tracker configuration
|
||||
* @returns {Array} Array of character objects in the format expected by the renderer
|
||||
*/
|
||||
function convertStructuredCharactersToFormat(charactersData, config) {
|
||||
if (!charactersData || !Array.isArray(charactersData) || charactersData.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enabledFields = config?.customFields?.filter(f => f && f.enabled && f.name) || [];
|
||||
const enabledCharStats = config?.characterStats?.enabled && config?.characterStats?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||
const thoughtsFieldName = config?.thoughts?.name || 'Thoughts';
|
||||
|
||||
return charactersData.map(char => {
|
||||
const result = {
|
||||
name: char.name || 'Unknown',
|
||||
emoji: char.emoji || '😶',
|
||||
fields: {},
|
||||
relationship: char.relationship || null,
|
||||
stats: {},
|
||||
thoughts: char.thoughts || ''
|
||||
};
|
||||
|
||||
// Map custom fields - check both top-level and nested in char.fields
|
||||
const charFields = char.fields || {};
|
||||
enabledFields.forEach(field => {
|
||||
const fieldId = field.id || field.name.toLowerCase().replace(/\s+/g, '_');
|
||||
// First check char.fields (LLM format), then check top-level
|
||||
if (charFields[field.name] !== undefined) {
|
||||
result.fields[field.name] = charFields[field.name];
|
||||
} else if (charFields[fieldId] !== undefined) {
|
||||
result.fields[field.name] = charFields[fieldId];
|
||||
} else if (char[fieldId] !== undefined) {
|
||||
result.fields[field.name] = char[fieldId];
|
||||
} else if (char[field.name] !== undefined) {
|
||||
result.fields[field.name] = char[field.name];
|
||||
}
|
||||
});
|
||||
|
||||
// Map character stats - check both nested and top-level
|
||||
const charStats = char.stats || {};
|
||||
if (enabledCharStats.length > 0) {
|
||||
enabledCharStats.forEach(stat => {
|
||||
const statId = stat.id || stat.name.toLowerCase().replace(/\s+/g, '_');
|
||||
if (charStats[stat.name] !== undefined) {
|
||||
result.stats[stat.name] = charStats[stat.name];
|
||||
} else if (charStats[statId] !== undefined) {
|
||||
result.stats[stat.name] = charStats[statId];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Also add description if present
|
||||
if (char.description) {
|
||||
result.description = char.description;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders characters using structured data format
|
||||
* @param {Array} characters - Parsed character data
|
||||
* @param {Object} config - Tracker config
|
||||
* @param {Array} enabledFields - Enabled custom fields
|
||||
* @param {Array} enabledCharStats - Enabled character stats
|
||||
* @param {Array} relationshipFields - Available relationship types
|
||||
* @param {boolean} hasRelationshipEnabled - Whether relationships are enabled
|
||||
*/
|
||||
function renderStructuredCharacters(characters, config, enabledFields, enabledCharStats, relationshipFields, hasRelationshipEnabled) {
|
||||
debugLog('[RPG Thoughts] Rendering structured characters:', characters.length);
|
||||
|
||||
const thoughtsFieldName = config?.thoughts?.name || 'Thoughts';
|
||||
const thoughtsEnabled = config?.thoughts?.enabled;
|
||||
|
||||
// Build HTML for each character
|
||||
let html = '<div class="rpg-present-characters">';
|
||||
|
||||
for (const char of characters) {
|
||||
const avatarUrl = getCharacterAvatarUrl(char.name);
|
||||
const relationshipEmoji = getRelationshipEmoji(char.relationship);
|
||||
|
||||
html += `
|
||||
<div class="rpg-character-card rpg-structured" data-character="${escapeHtmlAttr(char.name)}">
|
||||
<div class="rpg-character-header">
|
||||
<div class="rpg-character-avatar">
|
||||
<img src="${avatarUrl}" onerror="this.src='${FALLBACK_AVATAR_DATA_URI}'" alt="${escapeHtmlAttr(char.name)}">
|
||||
${char.relationship ? `<span class="rpg-relationship-badge" title="${escapeHtmlAttr(char.relationship)}">${relationshipEmoji}</span>` : ''}
|
||||
</div>
|
||||
<div class="rpg-character-info">
|
||||
<div class="rpg-character-name">
|
||||
<span class="rpg-char-emoji rpg-editable" contenteditable="true" data-character="${escapeHtmlAttr(char.name)}" data-field="emoji">${char.emoji}</span>
|
||||
<span class="rpg-char-name-text rpg-editable" contenteditable="true" data-character="${escapeHtmlAttr(char.name)}" data-field="name">${char.name}</span>
|
||||
</div>
|
||||
${char.description ? `<div class="rpg-character-description">${char.description}</div>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Custom fields - safely check if fields exists
|
||||
const charFields = char.fields || {};
|
||||
if (enabledFields.length > 0 && Object.keys(charFields).length > 0) {
|
||||
html += '<div class="rpg-character-fields">';
|
||||
for (const field of enabledFields) {
|
||||
const value = charFields[field.name] || '';
|
||||
if (value) {
|
||||
html += `
|
||||
<div class="rpg-character-field">
|
||||
<span class="rpg-field-label">${field.name}:</span>
|
||||
<span class="rpg-field-value rpg-editable" contenteditable="true" data-character="${escapeHtmlAttr(char.name)}" data-field="${field.name}">${value}</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Character stats (health, arousal, etc.) - safely check if stats exists
|
||||
const charStats = char.stats || {};
|
||||
if (enabledCharStats.length > 0 && Object.keys(charStats).length > 0) {
|
||||
html += '<div class="rpg-character-stats">';
|
||||
for (const stat of enabledCharStats) {
|
||||
const value = charStats[stat.name];
|
||||
if (value !== undefined) {
|
||||
const color = getStatColor(value, stat.lowColor || '#ff0000', stat.highColor || '#00ff00');
|
||||
html += `
|
||||
<div class="rpg-char-stat">
|
||||
<span class="rpg-stat-name">${stat.name}</span>
|
||||
<div class="rpg-stat-bar">
|
||||
<div class="rpg-stat-fill" style="width: ${value}%; background-color: ${color};"></div>
|
||||
</div>
|
||||
<span class="rpg-stat-value rpg-editable" contenteditable="true" data-character="${escapeHtmlAttr(char.name)}" data-field="${stat.name}">${value}%</span>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// Relationship field
|
||||
if (hasRelationshipEnabled && char.relationship) {
|
||||
// Add the character's relationship to options if not already in the list
|
||||
const allRelationships = [...relationshipFields];
|
||||
if (char.relationship && !allRelationships.includes(char.relationship)) {
|
||||
allRelationships.unshift(char.relationship);
|
||||
}
|
||||
html += `
|
||||
<div class="rpg-character-relationship">
|
||||
<span class="rpg-field-label">Relationship:</span>
|
||||
<select class="rpg-relationship-select" data-character="${escapeHtmlAttr(char.name)}" data-field="Relationship">
|
||||
${allRelationships.map(r => `<option value="${r}" ${char.relationship === r ? 'selected' : ''}>${r}</option>`).join('')}
|
||||
</select>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Thoughts
|
||||
if (thoughtsEnabled && char.thoughts) {
|
||||
html += `
|
||||
<div class="rpg-character-thoughts">
|
||||
<span class="rpg-thoughts-label">${thoughtsFieldName}:</span>
|
||||
<span class="rpg-thoughts-text rpg-editable" contenteditable="true" data-character="${escapeHtmlAttr(char.name)}" data-field="${thoughtsFieldName}">${char.thoughts}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// If no characters
|
||||
if (characters.length === 0) {
|
||||
html = '<div class="rpg-no-characters">No characters present</div>';
|
||||
}
|
||||
|
||||
$thoughtsContainer.html(html);
|
||||
|
||||
// Remove updating animation
|
||||
if (extensionSettings.enableAnimations) {
|
||||
setTimeout(() => $thoughtsContainer.removeClass('rpg-content-updating'), 300);
|
||||
}
|
||||
|
||||
// Setup event listeners for editable fields
|
||||
setupStructuredCharacterEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets relationship emoji from relationship string
|
||||
* Returns a default emoji (⚖️) if relationship is not in the predefined map
|
||||
@@ -330,55 +140,6 @@ function getCharacterAvatarUrl(characterName) {
|
||||
return FALLBACK_AVATAR_DATA_URI;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for structured character editing
|
||||
*/
|
||||
function setupStructuredCharacterEventListeners() {
|
||||
$thoughtsContainer.off('blur', '.rpg-editable').on('blur', '.rpg-editable', function() {
|
||||
const $this = $(this);
|
||||
const characterName = $this.data('character');
|
||||
const field = $this.data('field');
|
||||
const newValue = $this.text().trim();
|
||||
|
||||
// Update the charactersData
|
||||
const charIndex = extensionSettings.charactersData?.findIndex(c => c.name === characterName);
|
||||
if (charIndex !== undefined && charIndex !== -1) {
|
||||
const char = extensionSettings.charactersData[charIndex];
|
||||
|
||||
if (field === 'name') {
|
||||
char.name = newValue;
|
||||
} else if (field === 'emoji') {
|
||||
char.emoji = newValue;
|
||||
} else if (field === 'Thoughts' || field === extensionSettings.trackerConfig?.presentCharacters?.thoughts?.name) {
|
||||
char.thoughts = newValue;
|
||||
} else {
|
||||
// Custom field or stat
|
||||
const fieldId = field.toLowerCase().replace(/\s+/g, '_');
|
||||
if (char.stats && char.stats[fieldId] !== undefined) {
|
||||
char.stats[fieldId] = parseInt(newValue.replace('%', '')) || 0;
|
||||
} else {
|
||||
char[fieldId] = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
saveChatData();
|
||||
}
|
||||
});
|
||||
|
||||
// Relationship select
|
||||
$thoughtsContainer.off('change', '.rpg-relationship-select').on('change', '.rpg-relationship-select', function() {
|
||||
const characterName = $(this).data('character');
|
||||
const newValue = $(this).val();
|
||||
|
||||
const charIndex = extensionSettings.charactersData?.findIndex(c => c.name === characterName);
|
||||
if (charIndex !== undefined && charIndex !== -1) {
|
||||
extensionSettings.charactersData[charIndex].relationship = newValue;
|
||||
saveChatData();
|
||||
renderThoughts(); // Re-render to update badge
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function renderThoughts() {
|
||||
if (!extensionSettings.showCharacterThoughts || !$thoughtsContainer) {
|
||||
return;
|
||||
|
||||
@@ -57,8 +57,8 @@ export function buildUserStatsText() {
|
||||
|
||||
// Add inventory summary only if inventory is enabled
|
||||
if (extensionSettings.showInventory) {
|
||||
const inventorySummary = buildInventorySummary(stats.inventory);
|
||||
text += inventorySummary;
|
||||
const inventorySummary = buildInventorySummary(stats.inventory);
|
||||
text += inventorySummary;
|
||||
}
|
||||
|
||||
// Add skills if enabled AND not shown in separate tab
|
||||
@@ -82,19 +82,19 @@ export function renderUserStats() {
|
||||
const stats = extensionSettings.userStats;
|
||||
const config = extensionSettings.trackerConfig?.userStats || {
|
||||
customStats: [
|
||||
{ id: 'health', name: 'Health', enabled: true },
|
||||
{ id: 'satiety', name: 'Satiety', enabled: true },
|
||||
{ id: 'energy', name: 'Energy', enabled: true },
|
||||
{ id: 'hygiene', name: 'Hygiene', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', enabled: true }
|
||||
{ id: 'health', name: 'Health', description: '', enabled: true },
|
||||
{ id: 'satiety', name: 'Satiety', description: '', enabled: true },
|
||||
{ id: 'energy', name: 'Energy', description: '', enabled: true },
|
||||
{ id: 'hygiene', name: 'Hygiene', description: '', enabled: true },
|
||||
{ id: 'arousal', name: 'Arousal', description: '', enabled: true }
|
||||
],
|
||||
rpgAttributes: [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
],
|
||||
statusSection: { enabled: true, showMoodEmoji: true, customFields: ['Conditions'] },
|
||||
skillsSection: { enabled: false, label: 'Skills' }
|
||||
@@ -187,12 +187,12 @@ export function renderUserStats() {
|
||||
if (showRPGAttributes) {
|
||||
// Use attributes from config, with fallback to defaults if not configured
|
||||
const rpgAttributes = (config.rpgAttributes && config.rpgAttributes.length > 0) ? config.rpgAttributes : [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
];
|
||||
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
|
||||
|
||||
|
||||
@@ -34,10 +34,11 @@ export function setupDesktopTabs() {
|
||||
}
|
||||
|
||||
// Create tab navigation - conditionally show skills, inventory and quests tabs based on settings
|
||||
const skillsLabel = extensionSettings.trackerConfig?.userStats?.skillsSection?.label || 'Skills';
|
||||
const skillsTabHtml = extensionSettings.showSkills ? `
|
||||
<button class="rpg-tab-btn" data-tab="skills">
|
||||
<i class="fa-solid fa-star"></i>
|
||||
<span data-i18n-key="global.skills">Skills</span>
|
||||
<span>${skillsLabel}</span>
|
||||
</button>` : '';
|
||||
|
||||
const inventoryTabHtml = extensionSettings.showInventory ? `
|
||||
|
||||
+135
-27
@@ -130,12 +130,12 @@ function resetToDefaults() {
|
||||
],
|
||||
showRPGAttributes: true,
|
||||
rpgAttributes: [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
],
|
||||
statusSection: {
|
||||
enabled: true,
|
||||
@@ -210,10 +210,12 @@ function renderUserStatsTab() {
|
||||
html += '<div class="rpg-editor-stats-list" id="rpg-editor-stats-list">';
|
||||
|
||||
config.customStats.forEach((stat, index) => {
|
||||
const statDesc = stat.description || '';
|
||||
html += `
|
||||
<div class="rpg-editor-stat-item" data-index="${index}">
|
||||
<div class="rpg-editor-stat-item rpg-editor-item-with-desc" data-index="${index}">
|
||||
<input type="checkbox" ${stat.enabled ? 'checked' : ''} class="rpg-stat-toggle" data-index="${index}">
|
||||
<input type="text" value="${stat.name}" class="rpg-stat-name" data-index="${index}" placeholder="Stat Name">
|
||||
<input type="text" value="${statDesc}" class="rpg-stat-desc" data-index="${index}" placeholder="Description for AI">
|
||||
<button class="rpg-stat-remove" data-index="${index}" title="Remove stat"><i class="fa-solid fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
@@ -245,12 +247,12 @@ function renderUserStatsTab() {
|
||||
// Ensure rpgAttributes exists in the actual config (not just local fallback)
|
||||
if (!config.rpgAttributes || config.rpgAttributes.length === 0) {
|
||||
config.rpgAttributes = [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
];
|
||||
// Save the defaults back to the actual config
|
||||
extensionSettings.trackerConfig.userStats.rpgAttributes = config.rpgAttributes;
|
||||
@@ -259,10 +261,12 @@ function renderUserStatsTab() {
|
||||
const rpgAttributes = config.rpgAttributes;
|
||||
|
||||
rpgAttributes.forEach((attr, index) => {
|
||||
const attrDesc = attr.description || '';
|
||||
html += `
|
||||
<div class="rpg-editor-stat-item" data-index="${index}">
|
||||
<div class="rpg-editor-stat-item rpg-editor-item-with-desc" data-index="${index}">
|
||||
<input type="checkbox" ${attr.enabled ? 'checked' : ''} class="rpg-attr-toggle" data-index="${index}">
|
||||
<input type="text" value="${attr.name}" class="rpg-attr-name" data-index="${index}" placeholder="Attribute Name">
|
||||
<input type="text" value="${attrDesc}" class="rpg-attr-desc" data-index="${index}" placeholder="Description for AI">
|
||||
<button class="rpg-attr-remove" data-index="${index}" title="Remove attribute"><i class="fa-solid fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
@@ -308,8 +312,28 @@ function renderUserStatsTab() {
|
||||
html += `<input type="text" id="rpg-skills-label" value="${config.skillsSection.label}" class="rpg-text-input" placeholder="Skills">`;
|
||||
|
||||
html += `<label>${i18n.getTranslation('template.trackerEditorModal.userStatsTab.skillsListLabel')}</label>`;
|
||||
html += '<div class="rpg-editor-stats-list" id="rpg-editor-skills-list">';
|
||||
|
||||
// Handle both old format (string array) and new format (object array)
|
||||
const skillFields = config.skillsSection.customFields || [];
|
||||
html += `<input type="text" id="rpg-skills-fields" value="${skillFields.join(', ')}" class="rpg-text-input" placeholder="e.g., Stealth, Persuasion, Combat">`;
|
||||
skillFields.forEach((skill, index) => {
|
||||
// Support both old format (string) and new format (object)
|
||||
const skillName = typeof skill === 'string' ? skill : (skill.name || '');
|
||||
const skillDesc = typeof skill === 'string' ? '' : (skill.description || '');
|
||||
const skillEnabled = typeof skill === 'string' ? true : (skill.enabled !== false);
|
||||
|
||||
html += `
|
||||
<div class="rpg-editor-stat-item rpg-editor-skill-item" data-index="${index}">
|
||||
<input type="checkbox" ${skillEnabled ? 'checked' : ''} class="rpg-skill-toggle" data-index="${index}">
|
||||
<input type="text" value="${skillName}" class="rpg-skill-name" data-index="${index}" placeholder="Skill Category Name">
|
||||
<input type="text" value="${skillDesc}" class="rpg-skill-desc" data-index="${index}" placeholder="Description for AI">
|
||||
<button class="rpg-skill-remove" data-index="${index}" title="Remove skill category"><i class="fa-solid fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
html += `<button class="rpg-btn-secondary" id="rpg-add-skill"><i class="fa-solid fa-plus"></i> ${i18n.getTranslation('template.trackerEditorModal.userStatsTab.addSkillButton') || 'Add Skill Category'}</button>`;
|
||||
|
||||
html += '</div>';
|
||||
|
||||
@@ -327,6 +351,7 @@ function setupUserStatsListeners() {
|
||||
extensionSettings.trackerConfig.userStats.customStats.push({
|
||||
id: newId,
|
||||
name: 'New Stat',
|
||||
description: '',
|
||||
enabled: true
|
||||
});
|
||||
// Initialize value if doesn't exist
|
||||
@@ -355,23 +380,30 @@ function setupUserStatsListeners() {
|
||||
extensionSettings.trackerConfig.userStats.customStats[index].name = $(this).val();
|
||||
});
|
||||
|
||||
// Update stat description
|
||||
$('.rpg-stat-desc').off('blur').on('blur', function() {
|
||||
const index = $(this).data('index');
|
||||
extensionSettings.trackerConfig.userStats.customStats[index].description = $(this).val();
|
||||
});
|
||||
|
||||
// Add attribute
|
||||
$('#rpg-add-attr').off('click').on('click', function() {
|
||||
// Ensure rpgAttributes array exists with defaults if needed
|
||||
if (!extensionSettings.trackerConfig.userStats.rpgAttributes || extensionSettings.trackerConfig.userStats.rpgAttributes.length === 0) {
|
||||
extensionSettings.trackerConfig.userStats.rpgAttributes = [
|
||||
{ id: 'str', name: 'STR', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', enabled: true },
|
||||
{ id: 'con', name: 'CON', enabled: true },
|
||||
{ id: 'int', name: 'INT', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', enabled: true }
|
||||
{ id: 'str', name: 'STR', description: '', enabled: true },
|
||||
{ id: 'dex', name: 'DEX', description: '', enabled: true },
|
||||
{ id: 'con', name: 'CON', description: '', enabled: true },
|
||||
{ id: 'int', name: 'INT', description: '', enabled: true },
|
||||
{ id: 'wis', name: 'WIS', description: '', enabled: true },
|
||||
{ id: 'cha', name: 'CHA', description: '', enabled: true }
|
||||
];
|
||||
}
|
||||
const newId = 'attr_' + Date.now();
|
||||
extensionSettings.trackerConfig.userStats.rpgAttributes.push({
|
||||
id: newId,
|
||||
name: 'NEW',
|
||||
description: '',
|
||||
enabled: true
|
||||
});
|
||||
// Initialize value in classicStats if doesn't exist
|
||||
@@ -400,6 +432,12 @@ function setupUserStatsListeners() {
|
||||
extensionSettings.trackerConfig.userStats.rpgAttributes[index].name = $(this).val();
|
||||
});
|
||||
|
||||
// Update attribute description
|
||||
$('.rpg-attr-desc').off('blur').on('blur', function() {
|
||||
const index = $(this).data('index');
|
||||
extensionSettings.trackerConfig.userStats.rpgAttributes[index].description = $(this).val();
|
||||
});
|
||||
|
||||
// Enable/disable RPG Attributes section toggle
|
||||
$('#rpg-show-rpg-attrs').off('change').on('change', function() {
|
||||
extensionSettings.trackerConfig.userStats.showRPGAttributes = $(this).is(':checked');
|
||||
@@ -434,18 +472,80 @@ function setupUserStatsListeners() {
|
||||
});
|
||||
|
||||
$('#rpg-skills-label').off('blur').on('blur', function() {
|
||||
extensionSettings.trackerConfig.userStats.skillsSection.label = $(this).val();
|
||||
const newLabel = $(this).val();
|
||||
extensionSettings.trackerConfig.userStats.skillsSection.label = newLabel;
|
||||
saveSettings();
|
||||
renderUserStats();
|
||||
renderSkills();
|
||||
// Update the skills tab button text if it exists
|
||||
$('.rpg-tab-btn[data-tab="skills"] span').text(newLabel);
|
||||
});
|
||||
|
||||
$('#rpg-skills-fields').off('blur').on('blur', function() {
|
||||
const fields = $(this).val().split(',').map(f => f.trim()).filter(f => f);
|
||||
extensionSettings.trackerConfig.userStats.skillsSection.customFields = fields;
|
||||
// Add skill category
|
||||
$('#rpg-add-skill').off('click').on('click', function() {
|
||||
if (!extensionSettings.trackerConfig.userStats.skillsSection.customFields) {
|
||||
extensionSettings.trackerConfig.userStats.skillsSection.customFields = [];
|
||||
}
|
||||
extensionSettings.trackerConfig.userStats.skillsSection.customFields.push({
|
||||
id: 'skill_' + Date.now(),
|
||||
name: 'New Skill',
|
||||
description: '',
|
||||
enabled: true
|
||||
});
|
||||
renderUserStatsTab();
|
||||
saveSettings();
|
||||
// Re-render skills section when skills list changes
|
||||
renderSkills();
|
||||
});
|
||||
|
||||
// Remove skill category
|
||||
$('.rpg-skill-remove').off('click').on('click', function() {
|
||||
const index = $(this).data('index');
|
||||
extensionSettings.trackerConfig.userStats.skillsSection.customFields.splice(index, 1);
|
||||
renderUserStatsTab();
|
||||
saveSettings();
|
||||
renderSkills();
|
||||
});
|
||||
|
||||
// Toggle skill category
|
||||
$('.rpg-skill-toggle').off('change').on('change', function() {
|
||||
const index = $(this).data('index');
|
||||
ensureSkillIsObject(index);
|
||||
extensionSettings.trackerConfig.userStats.skillsSection.customFields[index].enabled = $(this).is(':checked');
|
||||
saveSettings();
|
||||
renderSkills();
|
||||
});
|
||||
|
||||
// Rename skill category
|
||||
$('.rpg-skill-name').off('blur').on('blur', function() {
|
||||
const index = $(this).data('index');
|
||||
ensureSkillIsObject(index);
|
||||
extensionSettings.trackerConfig.userStats.skillsSection.customFields[index].name = $(this).val();
|
||||
saveSettings();
|
||||
renderSkills();
|
||||
});
|
||||
|
||||
// Update skill description
|
||||
$('.rpg-skill-desc').off('blur').on('blur', function() {
|
||||
const index = $(this).data('index');
|
||||
ensureSkillIsObject(index);
|
||||
extensionSettings.trackerConfig.userStats.skillsSection.customFields[index].description = $(this).val();
|
||||
saveSettings();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to convert old string-format skill to object format
|
||||
*/
|
||||
function ensureSkillIsObject(index) {
|
||||
const skill = extensionSettings.trackerConfig.userStats.skillsSection.customFields[index];
|
||||
if (typeof skill === 'string') {
|
||||
extensionSettings.trackerConfig.userStats.skillsSection.customFields[index] = {
|
||||
id: 'skill_' + Date.now(),
|
||||
name: skill,
|
||||
description: '',
|
||||
enabled: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -663,7 +763,15 @@ function setupPresentCharactersListeners() {
|
||||
if (!extensionSettings.trackerConfig.presentCharacters.relationshipEmojis) {
|
||||
extensionSettings.trackerConfig.presentCharacters.relationshipEmojis = {};
|
||||
}
|
||||
extensionSettings.trackerConfig.presentCharacters.relationshipEmojis['New Relationship'] = '😊';
|
||||
|
||||
// Generate unique name to avoid overwriting existing "New Relationship" entries
|
||||
let newName = 'New Relationship';
|
||||
let counter = 1;
|
||||
while (extensionSettings.trackerConfig.presentCharacters.relationshipEmojis[newName]) {
|
||||
newName = `New Relationship ${counter}`;
|
||||
counter++;
|
||||
}
|
||||
extensionSettings.trackerConfig.presentCharacters.relationshipEmojis[newName] = '😊';
|
||||
|
||||
// Sync relationshipFields
|
||||
extensionSettings.trackerConfig.presentCharacters.relationshipFields =
|
||||
|
||||
@@ -1224,10 +1224,10 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
/* Allow wrapping for long day names */
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.rpg-calendar-year {
|
||||
@@ -1255,8 +1255,6 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
line-height: 1.1;
|
||||
word-wrap: break-word;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.rpg-weather-forecast.rpg-editable {
|
||||
@@ -2073,6 +2071,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
outline: 1px solid var(--rpg-highlight);
|
||||
}
|
||||
|
||||
/* Show full content on hover for text fields (not badges/icons) */
|
||||
.rpg-editable:not(.rpg-relationship-badge):not(.rpg-character-emoji):hover {
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
white-space: normal !important;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.rpg-editable:focus,
|
||||
.rpg-editable-stat:focus,
|
||||
.rpg-editable-stat-name:focus {
|
||||
@@ -2081,6 +2087,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
box-shadow: 0 0 8px var(--rpg-highlight);
|
||||
}
|
||||
|
||||
/* Show full content when focused for text fields (not badges/icons) */
|
||||
.rpg-editable:not(.rpg-relationship-badge):not(.rpg-character-emoji):focus {
|
||||
overflow: visible !important;
|
||||
text-overflow: clip !important;
|
||||
white-space: normal !important;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Edit button container and styling */
|
||||
.rpg-edit-button-container {
|
||||
display: flex;
|
||||
@@ -3682,6 +3696,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
@@ -3711,6 +3726,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
|
||||
.rpg-stat-remove,
|
||||
.rpg-attr-remove,
|
||||
.rpg-skill-remove,
|
||||
.rpg-remove-relationship {
|
||||
flex-shrink: 0;
|
||||
padding: 0.375em 0.625em;
|
||||
@@ -3722,6 +3738,48 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
/* Skills list spacing */
|
||||
#rpg-editor-skills-list {
|
||||
margin-top: 0.3em;
|
||||
}
|
||||
|
||||
/* Items with description field (stats, attrs, skills) */
|
||||
.rpg-editor-item-with-desc,
|
||||
.rpg-editor-skill-item {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.rpg-skill-name,
|
||||
.rpg-stat-desc,
|
||||
.rpg-attr-desc {
|
||||
flex: 1;
|
||||
min-width: 120px;
|
||||
padding: 0.375em 0.5em;
|
||||
background: var(--rpg-bg);
|
||||
border: 1px solid var(--rpg-border);
|
||||
border-radius: 0.25em;
|
||||
color: var(--rpg-text);
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.rpg-skill-desc,
|
||||
.rpg-stat-desc,
|
||||
.rpg-attr-desc {
|
||||
flex: 2;
|
||||
min-width: 180px;
|
||||
padding: 0.375em 0.5em;
|
||||
background: var(--rpg-bg);
|
||||
border: 1px solid var(--rpg-border);
|
||||
border-radius: 0.25em;
|
||||
color: var(--rpg-text);
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.rpg-skill-toggle {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rpg-stat-remove:hover,
|
||||
.rpg-attr-remove:hover,
|
||||
.rpg-remove-relationship:hover {
|
||||
|
||||
+1
-1
@@ -222,7 +222,7 @@
|
||||
<span data-i18n-key="template.settingsModal.display.deleteSkillWithItem">Delete skill when item removed</span>
|
||||
</label>
|
||||
<small style="display: block; margin-left: 72px; margin-top: -8px; color: #888; font-size: 11px;" data-i18n-key="template.settingsModal.display.deleteSkillWithItemNote">
|
||||
When disabled (default), removing an item just unlinks the skill. When enabled, the skill is deleted.
|
||||
When disabled, removing an item just unlinks the skill. When enabled, the skill is deleted.
|
||||
</small>
|
||||
|
||||
<label class="checkbox_label">
|
||||
|
||||
Reference in New Issue
Block a user