`;
}
-// Track open add forms (matching items/skills pattern)
-let openAddForms = {};
-
/**
* Main render function for quests
*/
export function renderQuests() {
- if (!extensionSettings.showQuests || !$questsContainer) {
+ if (!extensionSettings.showInventory || !$questsContainer) {
return;
}
// Get current sub-tab from container or default to 'main'
const activeSubTab = $questsContainer.data('active-subtab') || 'main';
+ // Get quests data
+ const mainQuest = extensionSettings.quests.main || 'None';
+ const optionalQuests = extensionSettings.quests.optional || [];
+
// Build HTML
let html = '
';
html += renderQuestsSubTabs(activeSubTab);
@@ -194,9 +178,9 @@ export function renderQuests() {
// Render active sub-tab
html += '
';
if (activeSubTab === 'main') {
- html += renderMainQuestView();
+ html += renderMainQuestView(mainQuest);
} else {
- html += renderOptionalQuestsView();
+ html += renderOptionalQuestsView(optionalQuests);
}
html += '
';
@@ -207,118 +191,117 @@ export function renderQuests() {
}
/**
- * Attach event handlers for quest interactions (matching items/skills pattern)
+ * Attach event handlers for quest interactions
*/
function attachQuestEventHandlers() {
// Sub-tab switching
- $questsContainer.find('.rpg-quests-subtab').off('click').on('click', function() {
+ $questsContainer.find('.rpg-quests-subtab').on('click', function() {
const tab = $(this).data('tab');
$questsContainer.data('active-subtab', tab);
renderQuests();
});
// Add quest button
- $questsContainer.find('[data-action="add-quest"]').off('click').on('click', function() {
+ $questsContainer.find('[data-action="add-quest"]').on('click', function() {
const field = $(this).data('field');
- openAddForms[field] = true;
- renderQuests();
- setTimeout(() => {
- $(`#rpg-new-quest-${field}`).focus();
- }, 50);
+ $(`#rpg-add-quest-form-${field}`).show();
+ $(`#rpg-new-quest-${field}`).focus();
});
// Cancel add quest
- $questsContainer.find('[data-action="cancel-add-quest"]').off('click').on('click', function() {
+ $questsContainer.find('[data-action="cancel-add-quest"]').on('click', function() {
const field = $(this).data('field');
- openAddForms[field] = false;
+ $(`#rpg-add-quest-form-${field}`).hide();
$(`#rpg-new-quest-${field}`).val('');
- renderQuests();
});
// Save add quest
- $questsContainer.find('[data-action="save-add-quest"]').off('click').on('click', function() {
+ $questsContainer.find('[data-action="save-add-quest"]').on('click', function() {
const field = $(this).data('field');
- const nameInput = $(`#rpg-new-quest-${field}`);
- const questTitle = nameInput.val().trim();
+ const input = $(`#rpg-new-quest-${field}`);
+ const questTitle = input.val().trim();
if (questTitle) {
- // Ensure structured format exists
- if (!extensionSettings.questsV2) {
- extensionSettings.questsV2 = { main: null, optional: [] };
- }
-
if (field === 'main') {
- extensionSettings.questsV2.main = { name: questTitle, description: '' };
+ extensionSettings.quests.main = questTitle;
} else {
- if (!extensionSettings.questsV2.optional) {
- extensionSettings.questsV2.optional = [];
+ if (!extensionSettings.quests.optional) {
+ extensionSettings.quests.optional = [];
}
- extensionSettings.questsV2.optional.push({ name: questTitle, description: '' });
+ extensionSettings.quests.optional.push(questTitle);
}
-
- openAddForms[field] = false;
saveSettings();
- saveChatData();
+ renderQuests();
+ }
+ });
+
+ // Edit quest (main only)
+ $questsContainer.find('[data-action="edit-quest"]').on('click', function() {
+ const field = $(this).data('field');
+ $(`#rpg-edit-quest-form-${field}`).show();
+ $('.rpg-quest-item[data-field="main"]').hide();
+ $(`#rpg-edit-quest-${field}`).focus();
+ });
+
+ // Cancel edit quest
+ $questsContainer.find('[data-action="cancel-edit-quest"]').on('click', function() {
+ const field = $(this).data('field');
+ $(`#rpg-edit-quest-form-${field}`).hide();
+ $('.rpg-quest-item[data-field="main"]').show();
+ });
+
+ // Save edit quest
+ $questsContainer.find('[data-action="save-edit-quest"]').on('click', function() {
+ const field = $(this).data('field');
+ const input = $(`#rpg-edit-quest-${field}`);
+ const questTitle = input.val().trim();
+
+ if (questTitle) {
+ extensionSettings.quests.main = questTitle;
+ saveSettings();
renderQuests();
}
});
// Remove quest
- $questsContainer.find('[data-action="remove-quest"]').off('click').on('click', function() {
+ $questsContainer.find('[data-action="remove-quest"]').on('click', function() {
const field = $(this).data('field');
const index = $(this).data('index');
if (field === 'main') {
- if (extensionSettings.questsV2) {
- extensionSettings.questsV2.main = null;
- }
+ extensionSettings.quests.main = 'None';
} else {
- if (extensionSettings.questsV2?.optional) {
- extensionSettings.questsV2.optional.splice(index, 1);
- }
+ extensionSettings.quests.optional.splice(index, 1);
}
saveSettings();
- saveChatData();
renderQuests();
});
- // Inline editing for quests (name and description) - matching items/skills pattern
- $questsContainer.off('blur', '.rpg-item-name.rpg-editable, .rpg-item-description.rpg-editable')
- .on('blur', '.rpg-item-name.rpg-editable, .rpg-item-description.rpg-editable', function() {
+ // Inline editing for optional quests
+ $questsContainer.find('.rpg-quest-title.rpg-editable').on('blur', function() {
const $this = $(this);
const field = $this.data('field');
const index = $this.data('index');
- const prop = $this.data('prop') || 'name';
- const newValue = $this.text().trim();
+ const newTitle = $this.text().trim();
- // Ensure structured format exists
- if (!extensionSettings.questsV2) {
- extensionSettings.questsV2 = { main: null, optional: [] };
+ if (newTitle && field === 'optional' && index !== undefined) {
+ extensionSettings.quests.optional[index] = newTitle;
+ saveSettings();
}
-
- if (field === 'main') {
- // Update main quest
- if (!extensionSettings.questsV2.main) {
- extensionSettings.questsV2.main = { name: '', description: '' };
- }
- extensionSettings.questsV2.main[prop] = newValue;
- } else if (field === 'optional' && index !== undefined) {
- // Update optional quest
- if (!extensionSettings.questsV2.optional[index]) {
- extensionSettings.questsV2.optional[index] = { name: '', description: '' };
- }
- extensionSettings.questsV2.optional[index][prop] = newValue;
- }
-
- saveSettings();
- saveChatData();
});
- // Enter key to save in forms (matching items/skills pattern)
- $questsContainer.find('.rpg-inline-input').off('keypress').on('keypress', function(e) {
+ // Enter key to save in forms
+ $questsContainer.find('.rpg-inline-input').on('keypress', function(e) {
if (e.which === 13) {
- const field = $(this).attr('id').replace('rpg-new-quest-', '');
- $(`[data-action="save-add-quest"][data-field="${field}"]`).click();
+ const field = $(this).attr('id').includes('edit') ?
+ $(this).attr('id').replace('rpg-edit-quest-', '') :
+ $(this).attr('id').replace('rpg-new-quest-', '');
+
+ if ($(this).attr('id').includes('edit')) {
+ $(`[data-action="save-edit-quest"][data-field="${field}"]`).click();
+ } else {
+ $(`[data-action="save-add-quest"][data-field="${field}"]`).click();
+ }
}
});
}
diff --git a/src/systems/rendering/skills.js b/src/systems/rendering/skills.js
deleted file mode 100644
index bdff0a5..0000000
--- a/src/systems/rendering/skills.js
+++ /dev/null
@@ -1,1052 +0,0 @@
-/**
- * Skills Rendering Module
- * Handles rendering of the skills section with skill categories (like inventory)
- * Each configured skill becomes a category, and abilities/items can be added within each
- */
-
-import { extensionSettings, $skillsContainer } from '../../core/state.js';
-import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
-import { i18n } from '../../core/i18n.js';
-import { parseItems } from '../../utils/itemParser.js';
-
-/**
- * Escapes HTML special characters to prevent XSS
- * @param {string} text - Text to escape
- * @returns {string} Escaped text
- */
-function escapeHtml(text) {
- if (!text) return '';
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
-}
-
-/**
- * Serializes an array of items into a comma-separated string
- * @param {string[]} items - Array of items
- * @returns {string} Comma-separated string or 'None'
- */
-function serializeItems(items) {
- if (!items || items.length === 0) return 'None';
- return items.join(', ');
-}
-
-/**
- * Gets the configured skill categories from settings
- * @returns {string[]} Array of skill category names
- */
-export function getSkillCategories() {
- const categories = extensionSettings.trackerConfig?.userStats?.skillsSection?.customFields || [];
- // Migration function handles string array → object array conversion on load
- return categories
- .filter(cat => cat.enabled !== false)
- .map(cat => cat.name);
-}
-
-/**
- * Gets the items/abilities for a specific skill category
- * Checks both skillsData (from parser) and skills.categories (manual entries)
- * @param {string} skillName - The skill category name
- * @returns {string} Comma-separated items string or 'None'
- */
-export function getSkillItems(skillName) {
- // Check skillsData first (populated by parser)
- if (extensionSettings.skillsData?.[skillName]) {
- return extensionSettings.skillsData[skillName];
- }
- // Fall back to skills.categories (manual entries)
- if (extensionSettings.skills?.categories?.[skillName]) {
- return extensionSettings.skills.categories[skillName];
- }
- return 'None';
-}
-
-/**
- * Sets the items/abilities for a specific skill category
- * @param {string} skillName - The skill category name
- * @param {string} itemsString - Comma-separated items string
- */
-export function setSkillItems(skillName, itemsString) {
- // Initialize structures if needed
- if (!extensionSettings.skillsData) {
- extensionSettings.skillsData = {};
- }
- if (!extensionSettings.skills) {
- extensionSettings.skills = { categories: {}, list: [] };
- }
- if (!extensionSettings.skills.categories) {
- extensionSettings.skills.categories = {};
- }
-
- // Store in both places for compatibility
- extensionSettings.skillsData[skillName] = itemsString || 'None';
- extensionSettings.skills.categories[skillName] = itemsString || 'None';
-
- saveSettings();
- saveChatData();
- updateMessageSwipeData();
-}
-
-/**
- * Adds an item to a skill category
- * @param {string} skillName - The skill category name
- * @param {string} item - The item to add
- */
-export function addSkillItem(skillName, item, description = '') {
- // Check for structured data first
- const skillsV2 = extensionSettings.skillsV2;
- if (skillsV2 && skillsV2[skillName] !== undefined) {
- if (!Array.isArray(skillsV2[skillName])) {
- skillsV2[skillName] = [];
- }
- // Check if ability already exists
- const exists = skillsV2[skillName].some(a => a.name === item);
- if (!exists) {
- skillsV2[skillName].push({ name: item, description: description, grantedBy: null });
- saveSettings();
- saveChatData();
- }
- return;
- }
-
- // Fall back to legacy format
- const currentItems = parseItems(getSkillItems(skillName));
- if (!currentItems.includes(item)) {
- currentItems.push(item);
- setSkillItems(skillName, serializeItems(currentItems));
- }
-}
-
-/**
- * Removes an item from a skill category
- * @param {string} skillName - The skill category name
- * @param {number} index - Index of item to remove
- */
-export function removeSkillItem(skillName, index) {
- // Check for structured data first
- const skillsV2 = extensionSettings.skillsV2;
- if (skillsV2 && skillsV2[skillName] !== undefined && Array.isArray(skillsV2[skillName])) {
- if (index >= 0 && index < skillsV2[skillName].length) {
- const removedAbility = skillsV2[skillName][index];
- skillsV2[skillName].splice(index, 1);
-
- // Remove any skill-ability links
- if (extensionSettings.skillAbilityLinks) {
- const linkKey = `${skillName}::${removedAbility.name}`;
- delete extensionSettings.skillAbilityLinks[linkKey];
- }
-
- saveSettings();
- saveChatData();
- }
- return;
- }
-
- // Fall back to legacy format
- const currentItems = parseItems(getSkillItems(skillName));
- if (index >= 0 && index < currentItems.length) {
- const removedItem = currentItems[index];
- currentItems.splice(index, 1);
- setSkillItems(skillName, serializeItems(currentItems));
-
- // Handle item-skill link removal if enabled
- if (extensionSettings.enableItemSkillLinks && extensionSettings.itemSkillLinks) {
- // Check if this item was linked and remove the link
- for (const [itemName, linkedSkill] of Object.entries(extensionSettings.itemSkillLinks)) {
- if (linkedSkill === skillName && itemName === removedItem) {
- delete extensionSettings.itemSkillLinks[itemName];
- break;
- }
- }
- }
- }
-}
-
-/**
- * Updates an item in a skill category
- * @param {string} skillName - The skill category name
- * @param {number} index - Index of item to update
- * @param {string} newValue - New item value
- */
-export function updateSkillItem(skillName, index, newValue) {
- // Check for structured data first
- const skillsV2 = extensionSettings.skillsV2;
- if (skillsV2 && skillsV2[skillName] && Array.isArray(skillsV2[skillName]) && skillsV2[skillName][index]) {
- skillsV2[skillName][index].name = newValue;
- saveSettings();
- saveChatData();
- return;
- }
-
- // Fall back to legacy format
- const currentItems = parseItems(getSkillItems(skillName));
- if (index >= 0 && index < currentItems.length) {
- currentItems[index] = newValue;
- setSkillItems(skillName, serializeItems(currentItems));
- }
-}
-
-/**
- * Updates a skill ability's description (structured format only)
- * @param {string} skillName - The skill category name
- * @param {number} index - Index of ability to update
- * @param {string} newDescription - New description
- */
-function updateStructuredSkillDescription(skillName, index, newDescription) {
- const skillsV2 = extensionSettings.skillsV2;
- if (skillsV2 && skillsV2[skillName] && Array.isArray(skillsV2[skillName]) && skillsV2[skillName][index]) {
- skillsV2[skillName][index].description = newDescription;
- saveSettings();
- saveChatData();
- }
-}
-
-/**
- * Called when an item is removed from inventory
- * Based on deleteSkillWithItem setting:
- * - false (default): Just removes the link, skill remains
- * - true: Deletes the linked skill abilities entirely
- * @param {string} itemName - The name of the removed item
- */
-export function handleItemRemoved(itemName) {
- if (!extensionSettings.enableItemSkillLinks) return;
- if (!extensionSettings.skillAbilityLinks) return;
-
- const itemNameLower = itemName.toLowerCase().trim();
- const linksToRemove = [];
-
- // Find all skill abilities linked to this item
- for (const [key, linkedItem] of Object.entries(extensionSettings.skillAbilityLinks)) {
- if (linkedItem && linkedItem.toLowerCase().trim() === itemNameLower) {
- linksToRemove.push(key);
- }
- }
-
- if (linksToRemove.length === 0) return;
-
- // Remove the links
- for (const key of linksToRemove) {
- delete extensionSettings.skillAbilityLinks[key];
-
- // If deleteSkillWithItem is enabled, also delete the skill ability itself
- if (extensionSettings.deleteSkillWithItem) {
- const [skillName, abilityName] = key.split('::');
- deleteSkillAbility(skillName, abilityName);
- }
- }
-
- saveSettings();
- saveChatData();
- renderSkills();
-}
-
-/**
- * Deletes a skill ability from the skills data
- * @param {string} skillName - The skill category name
- * @param {string} abilityName - The ability name to delete
- */
-function deleteSkillAbility(skillName, abilityName) {
- // Delete from structured skills (skillsV2)
- if (extensionSettings.skillsV2 && extensionSettings.skillsV2[skillName]) {
- const abilities = extensionSettings.skillsV2[skillName];
- if (Array.isArray(abilities)) {
- const index = abilities.findIndex(a =>
- (typeof a === 'string' ? a : a.name)?.toLowerCase().trim() === abilityName.toLowerCase().trim()
- );
- if (index !== -1) {
- abilities.splice(index, 1);
- }
- }
- }
-
- // Delete from legacy skillsData
- if (extensionSettings.skillsData && extensionSettings.skillsData[skillName]) {
- const currentItems = parseItems(extensionSettings.skillsData[skillName]);
- const index = currentItems.findIndex(item =>
- item.toLowerCase().trim() === abilityName.toLowerCase().trim()
- );
- if (index !== -1) {
- currentItems.splice(index, 1);
- extensionSettings.skillsData[skillName] = currentItems.length > 0 ? currentItems.join(', ') : 'None';
- }
- }
-}
-
-/**
- * Gets the linked item for a skill ability
- * @param {string} skillName - The skill category name
- * @param {string} abilityName - The ability name
- * @returns {string|null} The linked item name or null
- */
-export function getLinkedItem(skillName, abilityName) {
- if (!extensionSettings.skillAbilityLinks) return null;
- const key = `${skillName}::${abilityName}`;
- return extensionSettings.skillAbilityLinks[key] || null;
-}
-
-/**
- * Links a skill ability to an inventory item
- * @param {string} skillName - The skill category name
- * @param {string} abilityName - The ability name
- * @param {string} itemName - The inventory item name
- */
-export function linkAbilityToItem(skillName, abilityName, itemName) {
- if (!extensionSettings.skillAbilityLinks) {
- extensionSettings.skillAbilityLinks = {};
- }
- const key = `${skillName}::${abilityName}`;
- extensionSettings.skillAbilityLinks[key] = itemName;
- saveSettings();
- saveChatData();
-}
-
-/**
- * Unlinks a skill ability from its inventory item
- * @param {string} skillName - The skill category name
- * @param {string} abilityName - The ability name
- */
-export function unlinkAbility(skillName, abilityName) {
- if (!extensionSettings.skillAbilityLinks) return;
- const key = `${skillName}::${abilityName}`;
- delete extensionSettings.skillAbilityLinks[key];
- saveSettings();
- saveChatData();
-}
-
-/**
- * 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 (!itemName) return [];
- const linked = [];
- const normalizedItemName = itemName.toLowerCase().trim();
-
- // 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) {
- // 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;
-}
-
-/**
- * Navigates to the inventory tab and highlights an item
- * @param {string} itemName - The item to highlight
- */
-export function navigateToInventoryItem(itemName) {
- // Switch to inventory tab if on desktop
- if (window.innerWidth > 1000) {
- const $inventoryTab = $('.rpg-tab-btn[data-tab="inventory"]');
- if ($inventoryTab.length) {
- $inventoryTab.click();
- }
- }
-
- // Find and highlight the item after a delay for tab switch animation
- setTimeout(() => {
- // Search in inventory container specifically
- const $inventoryContainer = $('#rpg-inventory');
- const $items = $inventoryContainer.find('.rpg-item-name');
- let found = false;
-
- $items.each(function() {
- const text = $(this).text().trim();
- // Match exact or partial (for items that might have quantities etc)
- if (text === itemName || text.toLowerCase() === itemName.toLowerCase()) {
- const $row = $(this).closest('.rpg-item-row, .rpg-item-card');
- if ($row.length) {
- // Scroll into view
- $row[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
- // Add highlight class
- $row.addClass('rpg-highlight-item');
- found = true;
- // Remove after 3.5 seconds (after 3 animation cycles)
- setTimeout(() => {
- $row.removeClass('rpg-highlight-item');
- }, 3500);
- }
- return false; // Break the loop
- }
- });
-
- if (!found) {
- toastr.warning(`Item "${itemName}" not found in inventory`);
- }
- }, 300);
-}
-
-/**
- * Navigates to the skills tab and highlights abilities linked to an item
- * @param {string} itemName - The item whose linked abilities to highlight
- */
-export function navigateToLinkedSkills(itemName) {
- const linkedAbilities = getAbilitiesLinkedToItem(itemName);
- if (linkedAbilities.length === 0) {
- toastr.info(`No skills linked to "${itemName}"`);
- return;
- }
-
- // Switch to skills tab if on desktop
- if (window.innerWidth > 1000) {
- const $skillsTab = $('.rpg-tab-btn[data-tab="skills"]');
- if ($skillsTab.length) {
- $skillsTab.click();
- }
- }
-
- // Highlight all linked abilities after a delay for tab switch
- setTimeout(() => {
- let firstHighlighted = false;
-
- linkedAbilities.forEach(({ skillName, abilityName }) => {
- // Find the skill category
- const $category = $(`.rpg-skill-category[data-skill="${skillName}"]`);
- if ($category.length) {
- // Find items within this category
- const $items = $category.find('.rpg-item-row, .rpg-item-card');
- $items.each(function() {
- const $row = $(this);
- const $name = $row.find('.rpg-item-name');
- if ($name.text().trim() === abilityName) {
- // Scroll first match into view
- if (!firstHighlighted) {
- $row[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
- firstHighlighted = true;
- }
- // Add highlight class
- $row.addClass('rpg-highlight-item');
- // Remove after 3.5 seconds
- setTimeout(() => {
- $row.removeClass('rpg-highlight-item');
- }, 3500);
- }
- });
- }
- });
- }, 300);
-}
-
-/**
- * Gets all inventory items (from all categories) for linking dropdown
- * Supports both legacy (v2) and structured (v3) inventory formats
- * @returns {string[]} Array of item names
- */
-function getAllInventoryItems() {
- const items = [];
-
- // Check structured inventory (v3) first
- const inv3 = extensionSettings.inventoryV3;
- if (inv3) {
- // On Person
- if (inv3.onPerson && Array.isArray(inv3.onPerson)) {
- items.push(...inv3.onPerson.map(i => typeof i === 'string' ? i : i.name).filter(Boolean));
- }
- // Stored
- if (inv3.stored && typeof inv3.stored === 'object') {
- for (const locationItems of Object.values(inv3.stored)) {
- if (Array.isArray(locationItems)) {
- items.push(...locationItems.map(i => typeof i === 'string' ? i : i.name).filter(Boolean));
- }
- }
- }
- // Assets
- if (inv3.assets && Array.isArray(inv3.assets)) {
- items.push(...inv3.assets.map(i => typeof i === 'string' ? i : i.name).filter(Boolean));
- }
- }
-
- // Fall back to legacy inventory if no v3 items found
- if (items.length === 0) {
- const inventory = extensionSettings.userStats?.inventory;
- if (inventory) {
- // On Person
- if (inventory.onPerson && inventory.onPerson.toLowerCase() !== 'none') {
- items.push(...parseItems(inventory.onPerson));
- }
-
- // Stored locations
- if (inventory.stored && typeof inventory.stored === 'object') {
- for (const locationItems of Object.values(inventory.stored)) {
- if (locationItems && locationItems.toLowerCase() !== 'none') {
- items.push(...parseItems(locationItems));
- }
- }
- }
-
- // Assets
- if (inventory.assets && inventory.assets.toLowerCase() !== 'none') {
- items.push(...parseItems(inventory.assets));
- }
-
- // Simplified inventory
- if (inventory.items && inventory.items.toLowerCase() !== 'none') {
- items.push(...parseItems(inventory.items));
- }
- }
- }
-
- return [...new Set(items)];
-}
-
-// Track open add forms
-let openAddForms = {};
-
-/**
- * Shows the add item form for a skill category
- * @param {string} skillName - The skill category name
- */
-function showAddForm(skillName) {
- openAddForms[skillName] = true;
- renderSkills();
- // Focus the input after render
- setTimeout(() => {
- $(`#rpg-new-skill-item-${CSS.escape(skillName)}`).focus();
- }, 50);
-}
-
-/**
- * Hides the add item form for a skill category
- * @param {string} skillName - The skill category name
- */
-function hideAddForm(skillName) {
- openAddForms[skillName] = false;
- renderSkills();
-}
-
-/**
- * Saves a new item from the add form
- * @param {string} skillName - The skill category name
- */
-function saveAddItem(skillName) {
- const input = $(`#rpg-new-skill-item-${CSS.escape(skillName)}`);
- const value = input.val()?.trim();
- if (value) {
- addSkillItem(skillName, value);
- }
- hideAddForm(skillName);
-}
-
-/**
- * Renders a structured skill ability (with name + description)
- * @param {string} skillName - The skill category name
- * @param {Object} ability - Structured ability object {name, description, grantedBy}
- * @param {number} index - The item index
- * @param {string} viewMode - View mode ('list' or 'grid')
- * @returns {string} HTML string
- */
-function renderStructuredSkillAbility(skillName, ability, index, viewMode) {
- // Normalize ability - handle both string and object formats
- const normalizedAbility = typeof ability === 'string'
- ? { name: ability, description: '', grantedBy: null }
- : { name: ability?.name || 'Unknown', description: ability?.description || '', grantedBy: ability?.grantedBy || null };
-
- // Check for linked item - first from ability.grantedBy, then from skillAbilityLinks
- const linkedItem = normalizedAbility.grantedBy ||
- (extensionSettings.enableItemSkillLinks ? getLinkedItem(skillName, normalizedAbility.name) : null);
- const itemClass = viewMode === 'grid' ? 'rpg-item-card' : 'rpg-item-row';
- const hasLink = !!linkedItem;
-
- // Link indicator HTML - shows the item that grants this skill
- const linkIndicator = hasLink
- ? `
-
- ${escapeHtml(linkedItem)}
- `
- : (extensionSettings.enableItemSkillLinks
- ? `
-
- `
- : '');
-
- // Unlink button
- const unlinkBtn = hasLink && extensionSettings.enableItemSkillLinks
- ? `
-
- `
- : '';
-
- if (viewMode === 'list') {
- return `
-
`;
-
- // Check if skills are shown as separate section - if so, disable the toggle
- const skillsInSeparateTab = extensionSettings.showSkills;
- const skillsToggleDisabled = skillsInSeparateTab ? 'disabled' : '';
- const skillsToggleStyle = skillsInSeparateTab ? 'style="opacity: 0.5; cursor: not-allowed;"' : '';
-
- html += `
`;
- html += ``;
+ html += '
';
+ html += ``;
html += ``;
html += '
';
-
- // Show note when skills are in separate tab
- if (skillsInSeparateTab) {
- html += `${i18n.getTranslation('template.trackerEditorModal.userStatsTab.skillsInSeparateTabNote')}`;
- }
html += ``;
html += ``;
html += ``;
- html += '
@@ -198,43 +187,6 @@
Show Inventory
-
-
- Single flat list instead of On Person / Stored / Assets categories
-
-
-
-
- Displays skills as a separate tab instead of within Status. Configure skills in Edit Trackers.
-
-
-
-
- Items can grant skills. Removing an item unlinks or removes the skill.
-
-
-
-
- When disabled, removing an item just unlinks the skill. When enabled, the skill is deleted.
-
-
-
-
-
-
- When enabled, user messages are sent to the AI along with current RPG state and recent chat, then rewritten in-place.
-
-
-
-
-
- How many recent messages to send with the interception prompt.
-
-
@@ -334,28 +272,6 @@
When set, the extension will not inject tracker prompts, examples, or HTML instructions according to the selected mode when a guided generation (via `instruct` or `quiet_prompt`) is detected. Useful when using GuidedGenerations or similar extensions.
-
-
-
-
-
-
-
- Restore Default
-
-
-
- Customize the instructions sent to the AI for generating tracker data. Use {{user}} as a placeholder for the user's name. This is the main prompt that tells the AI how to format and update the RPG trackers.
-
-
-
-
-
-
-
-
-
-
- Restore Default
-
-
-
- Customize the instructions sent to the AI when rewriting user messages. Leave empty to use the default guidance. The AI receives this prompt, the current RPG state JSON, and the recent messages you specify above.
-
-