feat: add Equipment tab with slot-type validation
Add a new Equipment tab to manage player gear and stat bonuses. Features: - 19 equipment slots across 8 categories (helmet, necklace, body armor, gloves, pants, shoes, rings, accessories) - Type-to-slot validation: each type has max equipped limits (1 helmet, 10 rings, 3 accessories, etc.) - Auto-slot assignment: equipping a ring fills the first available ring slot - Stat bonuses from equipped items display on RPG attributes (e.g. STR 10 +2) - Create/edit modal with stat checkboxes per RPG attribute - Inventory list for unequipped items Architecture: - Shared constants in src/systems/equipment/constants.js - Category-based types (Ring, Accessory) with auto-slot assignment - v7 migration converts legacy slot-specific types to generic categories - Full i18n support for all UI strings Files: - New: src/systems/equipment/constants.js - New: src/systems/interaction/equipmentActions.js - New: src/systems/rendering/equipment.js - Modified: state.js, persistence.js, template.html, index.js - Modified: userStats.js, desktop.js, mobile.js, layout.js, modals.js - Modified: apiClient.js, sillytavern.js, style.css, en.json
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
|||||||
$infoBoxContainer,
|
$infoBoxContainer,
|
||||||
$thoughtsContainer,
|
$thoughtsContainer,
|
||||||
$inventoryContainer,
|
$inventoryContainer,
|
||||||
|
$equipmentContainer,
|
||||||
$questsContainer,
|
$questsContainer,
|
||||||
$musicPlayerContainer,
|
$musicPlayerContainer,
|
||||||
setExtensionSettings,
|
setExtensionSettings,
|
||||||
@@ -38,6 +39,7 @@ import {
|
|||||||
setInfoBoxContainer,
|
setInfoBoxContainer,
|
||||||
setThoughtsContainer,
|
setThoughtsContainer,
|
||||||
setInventoryContainer,
|
setInventoryContainer,
|
||||||
|
setEquipmentContainer,
|
||||||
setQuestsContainer,
|
setQuestsContainer,
|
||||||
setMusicPlayerContainer,
|
setMusicPlayerContainer,
|
||||||
clearSessionAvatarPrompts
|
clearSessionAvatarPrompts
|
||||||
@@ -69,6 +71,7 @@ import {
|
|||||||
createThoughtPanel
|
createThoughtPanel
|
||||||
} from './src/systems/rendering/thoughts.js';
|
} from './src/systems/rendering/thoughts.js';
|
||||||
import { renderInventory } from './src/systems/rendering/inventory.js';
|
import { renderInventory } from './src/systems/rendering/inventory.js';
|
||||||
|
import { renderEquipment } from './src/systems/rendering/equipment.js';
|
||||||
import { renderQuests } from './src/systems/rendering/quests.js';
|
import { renderQuests } from './src/systems/rendering/quests.js';
|
||||||
import { renderMusicPlayer } from './src/systems/rendering/musicPlayer.js';
|
import { renderMusicPlayer } from './src/systems/rendering/musicPlayer.js';
|
||||||
import { toggleSnowflakes, initSnowflakes } from './src/systems/ui/snowflakes.js';
|
import { toggleSnowflakes, initSnowflakes } from './src/systems/ui/snowflakes.js';
|
||||||
@@ -76,6 +79,7 @@ import { toggleDynamicWeather, initWeatherEffects, updateWeatherEffect } from '.
|
|||||||
|
|
||||||
// Interaction modules
|
// Interaction modules
|
||||||
import { initInventoryEventListeners } from './src/systems/interaction/inventoryActions.js';
|
import { initInventoryEventListeners } from './src/systems/interaction/inventoryActions.js';
|
||||||
|
import { initEquipmentEventListeners } from './src/systems/interaction/equipmentActions.js';
|
||||||
|
|
||||||
// UI Systems modules
|
// UI Systems modules
|
||||||
import {
|
import {
|
||||||
@@ -313,6 +317,7 @@ async function initUI() {
|
|||||||
setInfoBoxContainer($('#rpg-info-box'));
|
setInfoBoxContainer($('#rpg-info-box'));
|
||||||
setThoughtsContainer($('#rpg-thoughts'));
|
setThoughtsContainer($('#rpg-thoughts'));
|
||||||
setInventoryContainer($('#rpg-inventory'));
|
setInventoryContainer($('#rpg-inventory'));
|
||||||
|
setEquipmentContainer($('#rpg-equipment'));
|
||||||
setQuestsContainer($('#rpg-quests'));
|
setQuestsContainer($('#rpg-quests'));
|
||||||
setMusicPlayerContainer($('#rpg-music-player'));
|
setMusicPlayerContainer($('#rpg-music-player'));
|
||||||
|
|
||||||
@@ -389,6 +394,12 @@ async function initUI() {
|
|||||||
updateSectionVisibility();
|
updateSectionVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-equipment').on('change', function() {
|
||||||
|
extensionSettings.showEquipment = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateSectionVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
$('#rpg-toggle-quests').on('change', function() {
|
$('#rpg-toggle-quests').on('change', function() {
|
||||||
extensionSettings.showQuests = $(this).prop('checked');
|
extensionSettings.showQuests = $(this).prop('checked');
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -403,6 +414,7 @@ async function initUI() {
|
|||||||
renderInfoBox();
|
renderInfoBox();
|
||||||
renderThoughts();
|
renderThoughts();
|
||||||
renderInventory();
|
renderInventory();
|
||||||
|
renderEquipment();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -862,7 +874,7 @@ async function initUI() {
|
|||||||
if (lastAssistantIndex !== -1) {
|
if (lastAssistantIndex !== -1) {
|
||||||
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
||||||
}
|
}
|
||||||
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, renderEquipment);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Strip widget refresh button - same functionality as main refresh button
|
// Strip widget refresh button - same functionality as main refresh button
|
||||||
@@ -881,7 +893,7 @@ async function initUI() {
|
|||||||
if (lastAssistantIndex !== -1) {
|
if (lastAssistantIndex !== -1) {
|
||||||
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
||||||
}
|
}
|
||||||
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, renderEquipment);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#rpg-stat-bar-color-low').on('change', function() {
|
$('#rpg-stat-bar-color-low').on('change', function() {
|
||||||
@@ -1121,6 +1133,7 @@ async function initUI() {
|
|||||||
$('#rpg-toggle-thought-based-expressions').prop('checked', extensionSettings.enableThoughtBasedExpressions === true);
|
$('#rpg-toggle-thought-based-expressions').prop('checked', extensionSettings.enableThoughtBasedExpressions === true);
|
||||||
$('#rpg-toggle-hide-default-expressions').prop('checked', extensionSettings.hideDefaultExpressionDisplay === true);
|
$('#rpg-toggle-hide-default-expressions').prop('checked', extensionSettings.hideDefaultExpressionDisplay === true);
|
||||||
$('#rpg-toggle-inventory').prop('checked', extensionSettings.showInventory);
|
$('#rpg-toggle-inventory').prop('checked', extensionSettings.showInventory);
|
||||||
|
$('#rpg-toggle-equipment').prop('checked', extensionSettings.showEquipment);
|
||||||
$('#rpg-toggle-quests').prop('checked', extensionSettings.showQuests);
|
$('#rpg-toggle-quests').prop('checked', extensionSettings.showQuests);
|
||||||
$('#rpg-toggle-lock-icons').prop('checked', extensionSettings.showLockIcons ?? true);
|
$('#rpg-toggle-lock-icons').prop('checked', extensionSettings.showLockIcons ?? true);
|
||||||
$('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat);
|
$('#rpg-toggle-thoughts-in-chat').prop('checked', extensionSettings.showThoughtsInChat);
|
||||||
@@ -1271,6 +1284,7 @@ async function initUI() {
|
|||||||
renderInfoBox();
|
renderInfoBox();
|
||||||
renderThoughts();
|
renderThoughts();
|
||||||
renderInventory();
|
renderInventory();
|
||||||
|
renderEquipment();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
updateDiceDisplay();
|
updateDiceDisplay();
|
||||||
@@ -1284,6 +1298,7 @@ async function initUI() {
|
|||||||
setupMobileKeyboardHandling();
|
setupMobileKeyboardHandling();
|
||||||
setupContentEditableScrolling();
|
setupContentEditableScrolling();
|
||||||
initInventoryEventListeners();
|
initInventoryEventListeners();
|
||||||
|
initEquipmentEventListeners();
|
||||||
|
|
||||||
// Initialize chapter checkpoint UI
|
// Initialize chapter checkpoint UI
|
||||||
initChapterCheckpointUI();
|
initChapterCheckpointUI();
|
||||||
|
|||||||
+64
-1
@@ -23,7 +23,7 @@ import { validateStoredInventory, cleanItemString } from '../utils/security.js';
|
|||||||
import { migrateToV3JSON } from '../utils/jsonMigration.js';
|
import { migrateToV3JSON } from '../utils/jsonMigration.js';
|
||||||
|
|
||||||
const extensionName = 'third-party/rpg-companion-sillytavern';
|
const extensionName = 'third-party/rpg-companion-sillytavern';
|
||||||
const CURRENT_SETTINGS_VERSION = 5;
|
const CURRENT_SETTINGS_VERSION = 7;
|
||||||
|
|
||||||
const DEFAULT_USER_STATS = {
|
const DEFAULT_USER_STATS = {
|
||||||
health: 100,
|
health: 100,
|
||||||
@@ -616,6 +616,69 @@ export function loadSettings() {
|
|||||||
settingsChanged = true;
|
settingsChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration to version 6: Add equipment data structure
|
||||||
|
if (currentVersion < 6) {
|
||||||
|
// console.log('[RPG Companion] Migrating settings to version 6 (adding equipment)');
|
||||||
|
if (!extensionSettings.userStats.equipment) {
|
||||||
|
extensionSettings.userStats.equipment = {
|
||||||
|
items: [],
|
||||||
|
slots: {
|
||||||
|
helmet: null,
|
||||||
|
ring1: null,
|
||||||
|
ring2: null,
|
||||||
|
ring3: null,
|
||||||
|
ring4: null,
|
||||||
|
ring5: null,
|
||||||
|
ring6: null,
|
||||||
|
ring7: null,
|
||||||
|
ring8: null,
|
||||||
|
ring9: null,
|
||||||
|
ring10: null,
|
||||||
|
necklace: null,
|
||||||
|
bodyArmor: null,
|
||||||
|
pants: null,
|
||||||
|
shoes: null,
|
||||||
|
gloves: null,
|
||||||
|
accessory1: null,
|
||||||
|
accessory2: null,
|
||||||
|
accessory3: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (extensionSettings.showEquipment === undefined) {
|
||||||
|
extensionSettings.showEquipment = true;
|
||||||
|
}
|
||||||
|
extensionSettings.settingsVersion = 6;
|
||||||
|
settingsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migration to version 7: Convert equipment types to generic categories + add item.slot
|
||||||
|
if (currentVersion < 7) {
|
||||||
|
const equipment = extensionSettings.userStats?.equipment;
|
||||||
|
if (equipment) {
|
||||||
|
const typeMap = {
|
||||||
|
ring1: 'ring', ring2: 'ring', ring3: 'ring', ring4: 'ring', ring5: 'ring',
|
||||||
|
ring6: 'ring', ring7: 'ring', ring8: 'ring', ring9: 'ring', ring10: 'ring',
|
||||||
|
accessory1: 'accessory', accessory2: 'accessory', accessory3: 'accessory'
|
||||||
|
};
|
||||||
|
for (const item of equipment.items || []) {
|
||||||
|
if (!item.slot && equipment.slots) {
|
||||||
|
for (const [slotId, itemId] of Object.entries(equipment.slots)) {
|
||||||
|
if (itemId === item.id) {
|
||||||
|
item.slot = slotId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (item.type && typeMap[item.type]) {
|
||||||
|
item.type = typeMap[item.type];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extensionSettings.settingsVersion = 7;
|
||||||
|
settingsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Normalize additive settings without introducing another schema bump.
|
// Normalize additive settings without introducing another schema bump.
|
||||||
if (!extensionSettings.thoughtsInChatStyle) {
|
if (!extensionSettings.thoughtsInChatStyle) {
|
||||||
extensionSettings.thoughtsInChatStyle = 'corner';
|
extensionSettings.thoughtsInChatStyle = 'corner';
|
||||||
|
|||||||
+31
-1
@@ -10,7 +10,7 @@
|
|||||||
* Extension settings - persisted to SillyTavern settings
|
* Extension settings - persisted to SillyTavern settings
|
||||||
*/
|
*/
|
||||||
export let extensionSettings = {
|
export let extensionSettings = {
|
||||||
settingsVersion: 5, // Version number for settings migrations
|
settingsVersion: 6, // Version number for settings migrations
|
||||||
enabled: true,
|
enabled: true,
|
||||||
autoUpdate: false,
|
autoUpdate: false,
|
||||||
updateDepth: 4, // How many messages to include in the context
|
updateDepth: 4, // How many messages to include in the context
|
||||||
@@ -22,6 +22,7 @@ export let extensionSettings = {
|
|||||||
enableThoughtBasedExpressions: false,
|
enableThoughtBasedExpressions: false,
|
||||||
hideDefaultExpressionDisplay: false,
|
hideDefaultExpressionDisplay: false,
|
||||||
showInventory: true, // Show inventory section (v2 system)
|
showInventory: true, // Show inventory section (v2 system)
|
||||||
|
showEquipment: true, // Show equipment section
|
||||||
showQuests: true, // Show quests section
|
showQuests: true, // Show quests section
|
||||||
showThoughtsInChat: true, // Show thoughts overlay in chat
|
showThoughtsInChat: true, // Show thoughts overlay in chat
|
||||||
thoughtsInChatStyle: 'corner', // 'corner' or 'inline'
|
thoughtsInChatStyle: 'corner', // 'corner' or 'inline'
|
||||||
@@ -123,6 +124,30 @@ export let extensionSettings = {
|
|||||||
clothing: "None",
|
clothing: "None",
|
||||||
stored: {},
|
stored: {},
|
||||||
assets: "None"
|
assets: "None"
|
||||||
|
},
|
||||||
|
equipment: {
|
||||||
|
items: [], // Array of {id, name, type, slot, stats: {str: 2, dex: 1, ...}, description}
|
||||||
|
slots: {
|
||||||
|
helmet: null,
|
||||||
|
ring1: null,
|
||||||
|
ring2: null,
|
||||||
|
ring3: null,
|
||||||
|
ring4: null,
|
||||||
|
ring5: null,
|
||||||
|
ring6: null,
|
||||||
|
ring7: null,
|
||||||
|
ring8: null,
|
||||||
|
ring9: null,
|
||||||
|
ring10: null,
|
||||||
|
necklace: null,
|
||||||
|
bodyArmor: null,
|
||||||
|
pants: null,
|
||||||
|
shoes: null,
|
||||||
|
gloves: null,
|
||||||
|
accessory1: null,
|
||||||
|
accessory2: null,
|
||||||
|
accessory3: null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
statNames: {
|
statNames: {
|
||||||
@@ -472,6 +497,7 @@ export let $thoughtsContainer = null;
|
|||||||
export let $inventoryContainer = null;
|
export let $inventoryContainer = null;
|
||||||
export let $questsContainer = null;
|
export let $questsContainer = null;
|
||||||
export let $musicPlayerContainer = null;
|
export let $musicPlayerContainer = null;
|
||||||
|
export let $equipmentContainer = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State setters - provide controlled mutation of state variables
|
* State setters - provide controlled mutation of state variables
|
||||||
@@ -564,3 +590,7 @@ export function setQuestsContainer($element) {
|
|||||||
export function setMusicPlayerContainer($element) {
|
export function setMusicPlayerContainer($element) {
|
||||||
$musicPlayerContainer = $element;
|
$musicPlayerContainer = $element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setEquipmentContainer($element) {
|
||||||
|
$equipmentContainer = $element;
|
||||||
|
}
|
||||||
|
|||||||
@@ -459,6 +459,52 @@
|
|||||||
"global.locked": "Locked",
|
"global.locked": "Locked",
|
||||||
"global.unlocked": "Unlocked",
|
"global.unlocked": "Unlocked",
|
||||||
"global.confirm": "Confirm",
|
"global.confirm": "Confirm",
|
||||||
|
"global.equipment": "Equipment",
|
||||||
|
"equipment.title": "Equipment",
|
||||||
|
"equipment.createItem": "Create Equipment",
|
||||||
|
"equipment.createItemTitle": "Create Equipment",
|
||||||
|
"equipment.editItemTitle": "Edit Equipment",
|
||||||
|
"equipment.name": "Name",
|
||||||
|
"equipment.namePlaceholder": "Enter equipment name...",
|
||||||
|
"equipment.type": "Type",
|
||||||
|
"equipment.stats": "Stats",
|
||||||
|
"equipment.description": "Description",
|
||||||
|
"equipment.descriptionPlaceholder": "Enter description (optional)...",
|
||||||
|
"equipment.emptySlot": "Empty",
|
||||||
|
"equipment.unequip": "Unequip",
|
||||||
|
"equipment.equip": "Equip",
|
||||||
|
"equipment.editItem": "Edit item",
|
||||||
|
"equipment.deleteItem": "Delete item",
|
||||||
|
"equipment.inventoryTitle": "Inventory",
|
||||||
|
"equipment.slots.helmet": "Helmet",
|
||||||
|
"equipment.slots.necklace": "Necklace",
|
||||||
|
"equipment.slots.bodyArmor": "Body Armor",
|
||||||
|
"equipment.slots.gloves": "Gloves",
|
||||||
|
"equipment.slots.pants": "Pants",
|
||||||
|
"equipment.slots.shoes": "Shoes",
|
||||||
|
"equipment.slots.ring1": "Ring 1",
|
||||||
|
"equipment.slots.ring2": "Ring 2",
|
||||||
|
"equipment.slots.ring3": "Ring 3",
|
||||||
|
"equipment.slots.ring4": "Ring 4",
|
||||||
|
"equipment.slots.ring5": "Ring 5",
|
||||||
|
"equipment.slots.ring6": "Ring 6",
|
||||||
|
"equipment.slots.ring7": "Ring 7",
|
||||||
|
"equipment.slots.ring8": "Ring 8",
|
||||||
|
"equipment.slots.ring9": "Ring 9",
|
||||||
|
"equipment.slots.ring10": "Ring 10",
|
||||||
|
"equipment.slots.accessory1": "Accessory 1",
|
||||||
|
"equipment.slots.accessory2": "Accessory 2",
|
||||||
|
"equipment.slots.accessory3": "Accessory 3",
|
||||||
|
"equipment.types.helmet": "Helmet",
|
||||||
|
"equipment.types.necklace": "Necklace",
|
||||||
|
"equipment.types.bodyArmor": "Body Armor",
|
||||||
|
"equipment.types.gloves": "Gloves",
|
||||||
|
"equipment.types.pants": "Pants",
|
||||||
|
"equipment.types.shoes": "Shoes",
|
||||||
|
"equipment.types.ring": "Ring",
|
||||||
|
"equipment.types.accessory": "Accessory",
|
||||||
|
"template.settingsModal.display.showEquipment": "Show Equipment",
|
||||||
|
"template.settingsModal.display.showEquipmentNote": "Manage equipped gear and stat bonuses from items.",
|
||||||
"inventory.addItemPlaceholder": "Enter item name...",
|
"inventory.addItemPlaceholder": "Enter item name...",
|
||||||
"inventory.stored.removeLocationConfirm": "Remove \"{location}\"? This will delete all items stored there.",
|
"inventory.stored.removeLocationConfirm": "Remove \"{location}\"? This will delete all items stored there.",
|
||||||
"userStats.clickToEdit": "Click to edit",
|
"userStats.clickToEdit": "Click to edit",
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Equipment Constants
|
||||||
|
* Shared definitions for the equipment system
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equipment category definitions: maps type to allowed slots, max equipped, and icon
|
||||||
|
*/
|
||||||
|
export const EQUIPMENT_CATEGORIES = {
|
||||||
|
helmet: { slots: ['helmet'], maxEquipped: 1, icon: 'fa-hat-cowboy-side' },
|
||||||
|
necklace: { slots: ['necklace'], maxEquipped: 1, icon: 'fa-circle-nodes' },
|
||||||
|
bodyArmor: { slots: ['bodyArmor'], maxEquipped: 1, icon: 'fa-vest' },
|
||||||
|
gloves: { slots: ['gloves'], maxEquipped: 1, icon: 'fa-hand' },
|
||||||
|
pants: { slots: ['pants'], maxEquipped: 1, icon: 'fa-socks' },
|
||||||
|
shoes: { slots: ['shoes'], maxEquipped: 1, icon: 'fa-shoe-prints' },
|
||||||
|
ring: { slots: ['ring1', 'ring2', 'ring3', 'ring4', 'ring5', 'ring6', 'ring7', 'ring8', 'ring9', 'ring10'], maxEquipped: 10, icon: 'fa-ring' },
|
||||||
|
accessory: { slots: ['accessory1', 'accessory2', 'accessory3'], maxEquipped: 3, icon: 'fa-gem' }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flat list of all slots with their category info
|
||||||
|
*/
|
||||||
|
export const SLOTS_LIST = Object.entries(EQUIPMENT_CATEGORIES).flatMap(([type, def]) =>
|
||||||
|
def.slots.map(slotId => ({
|
||||||
|
id: slotId,
|
||||||
|
type,
|
||||||
|
icon: def.icon
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes HTML special characters to prevent XSS
|
||||||
|
* @param {string} text - Text to escape
|
||||||
|
* @returns {string} Escaped text
|
||||||
|
*/
|
||||||
|
export function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ import { renderInfoBox } from '../rendering/infoBox.js';
|
|||||||
import { removeLocks } from './lockManager.js';
|
import { removeLocks } from './lockManager.js';
|
||||||
import { renderThoughts } from '../rendering/thoughts.js';
|
import { renderThoughts } from '../rendering/thoughts.js';
|
||||||
import { renderInventory } from '../rendering/inventory.js';
|
import { renderInventory } from '../rendering/inventory.js';
|
||||||
|
import { renderEquipment } from '../rendering/equipment.js';
|
||||||
import { renderQuests } from '../rendering/quests.js';
|
import { renderQuests } from '../rendering/quests.js';
|
||||||
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
@@ -356,6 +357,7 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
renderInfoBox();
|
renderInfoBox();
|
||||||
renderThoughts();
|
renderThoughts();
|
||||||
renderInventory();
|
renderInventory();
|
||||||
|
renderEquipment();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { renderUserStats } from '../rendering/userStats.js';
|
|||||||
import { renderInfoBox } from '../rendering/infoBox.js';
|
import { renderInfoBox } from '../rendering/infoBox.js';
|
||||||
import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js';
|
import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js';
|
||||||
import { renderInventory } from '../rendering/inventory.js';
|
import { renderInventory } from '../rendering/inventory.js';
|
||||||
|
import { renderEquipment } from '../rendering/equipment.js';
|
||||||
import { renderQuests } from '../rendering/quests.js';
|
import { renderQuests } from '../rendering/quests.js';
|
||||||
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
import { renderMusicPlayer } from '../rendering/musicPlayer.js';
|
||||||
|
|
||||||
@@ -327,6 +328,7 @@ function rerenderRpgState() {
|
|||||||
renderInfoBox();
|
renderInfoBox();
|
||||||
renderThoughts();
|
renderThoughts();
|
||||||
renderInventory();
|
renderInventory();
|
||||||
|
renderEquipment();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
updateFabWidgets();
|
updateFabWidgets();
|
||||||
@@ -549,6 +551,7 @@ export async function onMessageReceived(data) {
|
|||||||
renderInfoBox();
|
renderInfoBox();
|
||||||
renderThoughts();
|
renderThoughts();
|
||||||
renderInventory();
|
renderInventory();
|
||||||
|
renderEquipment();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
|
|
||||||
@@ -770,6 +773,7 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
renderInfoBox();
|
renderInfoBox();
|
||||||
renderThoughts();
|
renderThoughts();
|
||||||
renderInventory();
|
renderInventory();
|
||||||
|
renderEquipment();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,578 @@
|
|||||||
|
/**
|
||||||
|
* Equipment Actions Module
|
||||||
|
* Handles all user interactions with the equipment system
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
||||||
|
import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
|
||||||
|
import { renderEquipment } from '../rendering/equipment.js';
|
||||||
|
import { renderUserStats } from '../rendering/userStats.js';
|
||||||
|
import { EQUIPMENT_CATEGORIES, escapeHtml } from '../equipment/constants.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given slot is currently occupied by any item
|
||||||
|
* @param {string} slotId - Slot to check
|
||||||
|
* @param {Array} items - Equipment items array
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isSlotOccupied(slotId, items) {
|
||||||
|
return items.some(item => item.slot === slotId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the first available slot for a given equipment type
|
||||||
|
* @param {string} type - Equipment type (helmet, ring, accessory, etc.)
|
||||||
|
* @param {Array} items - Equipment items array
|
||||||
|
* @returns {string|null} Available slot ID or null if all full
|
||||||
|
*/
|
||||||
|
function findAvailableSlot(type, items) {
|
||||||
|
const category = EQUIPMENT_CATEGORIES[type];
|
||||||
|
if (!category) return null;
|
||||||
|
return category.slots.find(slot => !isSlotOccupied(slot, items)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the slot ID currently assigned to an item
|
||||||
|
* @param {Object} item - Equipment item
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
function getItemSlot(item) {
|
||||||
|
return item.slot || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert old specific slot type (e.g. 'ring3') to generic category (e.g. 'ring')
|
||||||
|
* @param {string} type - Equipment type
|
||||||
|
* @returns {string} Generic type
|
||||||
|
*/
|
||||||
|
function normalizeType(type) {
|
||||||
|
if (!type) return type;
|
||||||
|
if (type.startsWith('ring') && type !== 'ring') return 'ring';
|
||||||
|
if (type.startsWith('accessory') && type !== 'accessory') return 'accessory';
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate old item types in the entire items array (for v6 → v7 migration)
|
||||||
|
* @param {Array} items - Equipment items array
|
||||||
|
*/
|
||||||
|
function migrateItemTypes(items) {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type) {
|
||||||
|
item.type = normalizeType(item.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique ID for equipment items
|
||||||
|
* @returns {string} Unique ID
|
||||||
|
*/
|
||||||
|
function generateItemId() {
|
||||||
|
return 'eq_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates lastGeneratedData and committedTrackerData to include current equipment state
|
||||||
|
*/
|
||||||
|
function updateEquipmentData() {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
const currentData = lastGeneratedData.userStats || committedTrackerData.userStats;
|
||||||
|
|
||||||
|
if (currentData) {
|
||||||
|
const trimmed = currentData.trim();
|
||||||
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const jsonData = JSON.parse(currentData);
|
||||||
|
if (jsonData && typeof jsonData === 'object') {
|
||||||
|
jsonData.equipment = JSON.parse(JSON.stringify(equipment));
|
||||||
|
const updatedJSON = JSON.stringify(jsonData, null, 2);
|
||||||
|
lastGeneratedData.userStats = updatedJSON;
|
||||||
|
committedTrackerData.userStats = updatedJSON;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG Equipment] Failed to parse JSON, falling back to text format:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: rebuild text format
|
||||||
|
const stats = extensionSettings.userStats;
|
||||||
|
const config = extensionSettings.trackerConfig?.userStats || {};
|
||||||
|
let text = '';
|
||||||
|
|
||||||
|
const enabledStats = config.customStats?.filter(stat => stat && stat.enabled && stat.name && stat.id) || [];
|
||||||
|
for (const stat of enabledStats) {
|
||||||
|
const value = stats[stat.id] !== undefined ? stats[stat.id] : 100;
|
||||||
|
text += `${stat.name}: ${value}%\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.statusSection?.enabled) {
|
||||||
|
if (config.statusSection.showMoodEmoji) {
|
||||||
|
text += `${stats.mood}: `;
|
||||||
|
}
|
||||||
|
text += `${stats.conditions || 'None'}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include equipment data in fallback
|
||||||
|
const equipped = equipment.items.filter(item => item.slot);
|
||||||
|
if (equipped.length > 0) {
|
||||||
|
text += `\nEquipment:\n`;
|
||||||
|
for (const item of equipped) {
|
||||||
|
const bonuses = Object.entries(item.stats || {})
|
||||||
|
.filter(([_, val]) => val > 0)
|
||||||
|
.map(([key, val]) => `${key.toUpperCase()}+${val}`)
|
||||||
|
.join(' ');
|
||||||
|
text += `- ${item.slot}: ${item.name}${bonuses ? ' (' + bonuses + ')' : ''}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastGeneratedData.userStats = text.trim();
|
||||||
|
committedTrackerData.userStats = text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the equipment creation modal
|
||||||
|
*/
|
||||||
|
export function showCreateModal() {
|
||||||
|
const modalHtml = generateCreateModalHTML();
|
||||||
|
$('body').append(modalHtml);
|
||||||
|
|
||||||
|
const $modal = $('#rpg-equipment-modal');
|
||||||
|
$modal.hide().fadeIn(200);
|
||||||
|
|
||||||
|
$('#rpg-eq-name').focus();
|
||||||
|
|
||||||
|
initStatCheckboxes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the equipment edit modal
|
||||||
|
* @param {string} itemId - ID of the item to edit
|
||||||
|
*/
|
||||||
|
export function showEditModal(itemId) {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
const item = equipment.items.find(i => i.id === itemId);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const modalHtml = generateEditModalHTML(item);
|
||||||
|
$('body').append(modalHtml);
|
||||||
|
|
||||||
|
const $modal = $('#rpg-equipment-modal');
|
||||||
|
$modal.hide().fadeIn(200);
|
||||||
|
|
||||||
|
initStatCheckboxes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the equipment modal
|
||||||
|
*/
|
||||||
|
export function closeModal() {
|
||||||
|
$('#rpg-equipment-modal').fadeOut(150, function() {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates HTML for the create modal
|
||||||
|
* @returns {string} Modal HTML
|
||||||
|
*/
|
||||||
|
function generateCreateModalHTML() {
|
||||||
|
const attributes = getAvailableAttributes();
|
||||||
|
const typeOptions = generateTypeOptions(null);
|
||||||
|
const statCheckboxes = generateStatCheckboxes(attributes, null);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div id="rpg-equipment-modal" class="rpg-equipment-modal" role="dialog" aria-modal="true">
|
||||||
|
<div class="rpg-equipment-modal-content">
|
||||||
|
<header class="rpg-equipment-modal-header">
|
||||||
|
<h3>
|
||||||
|
<i class="fa-solid fa-shield-halved"></i>
|
||||||
|
<span>${i18n.getTranslation('equipment.createItemTitle') || 'Create Equipment'}</span>
|
||||||
|
</h3>
|
||||||
|
<button id="rpg-equipment-modal-close" class="rpg-popup-close" type="button">
|
||||||
|
<i class="fa-solid fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div class="rpg-equipment-modal-body">
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label for="rpg-eq-name">${i18n.getTranslation('equipment.name') || 'Name'}:</label>
|
||||||
|
<input type="text" id="rpg-eq-name" class="rpg-input" maxlength="50" placeholder="${i18n.getTranslation('equipment.namePlaceholder') || 'Enter equipment name...'}" />
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label for="rpg-eq-type">${i18n.getTranslation('equipment.type') || 'Type'}:</label>
|
||||||
|
<select id="rpg-eq-type" class="rpg-select">${typeOptions}</select>
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label>${i18n.getTranslation('equipment.stats') || 'Stats'}:</label>
|
||||||
|
<div class="rpg-eq-stats-grid">${statCheckboxes}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label for="rpg-eq-description">${i18n.getTranslation('equipment.description') || 'Description'}:</label>
|
||||||
|
<textarea id="rpg-eq-description" class="rpg-textarea" maxlength="200" rows="2" placeholder="${i18n.getTranslation('equipment.descriptionPlaceholder') || 'Enter description (optional)...'}"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="rpg-equipment-modal-footer">
|
||||||
|
<button id="rpg-equipment-modal-cancel" class="rpg-btn-secondary" type="button">
|
||||||
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
|
</button>
|
||||||
|
<button id="rpg-equipment-modal-save" class="rpg-btn-primary" type="button">
|
||||||
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.save') || 'Save'}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates HTML for the edit modal
|
||||||
|
* @param {Object} item - The item to edit
|
||||||
|
* @returns {string} Modal HTML
|
||||||
|
*/
|
||||||
|
function generateEditModalHTML(item) {
|
||||||
|
const attributes = getAvailableAttributes();
|
||||||
|
const typeOptions = generateTypeOptions(item.type);
|
||||||
|
const statCheckboxes = generateStatCheckboxes(attributes, item);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div id="rpg-equipment-modal" class="rpg-equipment-modal" role="dialog" aria-modal="true">
|
||||||
|
<div class="rpg-equipment-modal-content">
|
||||||
|
<header class="rpg-equipment-modal-header">
|
||||||
|
<h3>
|
||||||
|
<i class="fa-solid fa-shield-halved"></i>
|
||||||
|
<span>${i18n.getTranslation('equipment.editItemTitle') || 'Edit Equipment'}</span>
|
||||||
|
</h3>
|
||||||
|
<button id="rpg-equipment-modal-close" class="rpg-popup-close" type="button">
|
||||||
|
<i class="fa-solid fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div class="rpg-equipment-modal-body">
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label for="rpg-eq-name">${i18n.getTranslation('equipment.name') || 'Name'}:</label>
|
||||||
|
<input type="text" id="rpg-eq-name" class="rpg-input" maxlength="50" value="${escapeHtml(item.name)}" placeholder="${i18n.getTranslation('equipment.namePlaceholder') || 'Enter equipment name...'}" />
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label for="rpg-eq-type">${i18n.getTranslation('equipment.type') || 'Type'}:</label>
|
||||||
|
<select id="rpg-eq-type" class="rpg-select">${typeOptions}</select>
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label>${i18n.getTranslation('equipment.stats') || 'Stats'}:</label>
|
||||||
|
<div class="rpg-eq-stats-grid">${statCheckboxes}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label for="rpg-eq-description">${i18n.getTranslation('equipment.description') || 'Description'}:</label>
|
||||||
|
<textarea id="rpg-eq-description" class="rpg-textarea" maxlength="200" rows="2" placeholder="${i18n.getTranslation('equipment.descriptionPlaceholder') || 'Enter description (optional)...'}">${escapeHtml(item.description || '')}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="rpg-equipment-modal-footer">
|
||||||
|
<button id="rpg-equipment-modal-cancel" class="rpg-btn-secondary" type="button">
|
||||||
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
|
</button>
|
||||||
|
<button id="rpg-equipment-modal-save" class="rpg-btn-primary" type="button" data-edit-id="${item.id}">
|
||||||
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.save') || 'Save'}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates type dropdown options
|
||||||
|
* @param {string|null} selectedType - Currently selected type or null
|
||||||
|
* @returns {string} HTML options
|
||||||
|
*/
|
||||||
|
function generateTypeOptions(selectedType) {
|
||||||
|
const types = Object.entries(EQUIPMENT_CATEGORIES).map(([value, def]) => ({
|
||||||
|
value,
|
||||||
|
label: def.slots[0] === value
|
||||||
|
? def.slots[0].charAt(0).toUpperCase() + def.slots[0].slice(1)
|
||||||
|
: value.charAt(0).toUpperCase() + value.slice(1) + (def.slots.length > 1 ? ` (max ${def.maxEquipped})` : '')
|
||||||
|
}));
|
||||||
|
|
||||||
|
return types.map(type => {
|
||||||
|
const selected = selectedType === type.value ? 'selected' : '';
|
||||||
|
return `<option value="${type.value}" ${selected}>${type.label}</option>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates stat checkboxes HTML
|
||||||
|
* @param {string[]} attributes - Available attribute IDs
|
||||||
|
* @param {Object|null} item - Current item for editing or null
|
||||||
|
* @returns {string} HTML for stat checkboxes
|
||||||
|
*/
|
||||||
|
function generateStatCheckboxes(attributes, item) {
|
||||||
|
return attributes.map(attr => {
|
||||||
|
const checked = item && item.stats && item.stats[attr] > 0;
|
||||||
|
const val = checked ? item.stats[attr] : 1;
|
||||||
|
return `
|
||||||
|
<label class="rpg-eq-stat-checkbox">
|
||||||
|
<input type="checkbox" class="rpg-eq-stat-check" data-attr="${attr}" ${checked ? 'checked' : ''} />
|
||||||
|
<span class="rpg-eq-stat-check-label">${attr.toUpperCase()}</span>
|
||||||
|
<input type="number" class="rpg-eq-stat-value-input" data-attr="${attr}" value="${val}" min="1" max="20" ${!checked ? 'disabled' : ''} />
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets available RPG attribute IDs from config
|
||||||
|
* @returns {string[]} Array of attribute IDs
|
||||||
|
*/
|
||||||
|
function getAvailableAttributes() {
|
||||||
|
const config = extensionSettings.trackerConfig?.userStats || {};
|
||||||
|
const rpgAttributes = config.rpgAttributes || [];
|
||||||
|
return rpgAttributes
|
||||||
|
.filter(attr => attr && attr.enabled && attr.id)
|
||||||
|
.map(attr => attr.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes stat checkbox change handlers
|
||||||
|
*/
|
||||||
|
function initStatCheckboxes() {
|
||||||
|
$('.rpg-eq-stat-check').on('change', function() {
|
||||||
|
const $input = $(this).siblings('.rpg-eq-stat-value-input');
|
||||||
|
$input.prop('disabled', !$(this).prop('checked'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the equipment item from the modal form
|
||||||
|
*/
|
||||||
|
export function saveEquipmentItem() {
|
||||||
|
const name = $('#rpg-eq-name').val().trim();
|
||||||
|
const type = $('#rpg-eq-type').val();
|
||||||
|
const description = $('#rpg-eq-description').val().trim();
|
||||||
|
const editId = $('#rpg-equipment-modal-save').data('edit-id');
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
$('#rpg-eq-name').focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect stats
|
||||||
|
const stats = {};
|
||||||
|
$('.rpg-eq-stat-check:checked').each(function() {
|
||||||
|
const attr = $(this).data('attr');
|
||||||
|
const val = parseInt($(this).siblings('.rpg-eq-stat-value-input').val()) || 1;
|
||||||
|
stats[attr] = Math.max(1, Math.min(20, val));
|
||||||
|
});
|
||||||
|
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
|
||||||
|
// Migrate old types (ring1-ring10 -> ring, accessory1-3 -> accessory)
|
||||||
|
migrateItemTypes(equipment.items);
|
||||||
|
|
||||||
|
if (editId) {
|
||||||
|
// Edit existing item
|
||||||
|
const item = equipment.items.find(i => i.id === editId);
|
||||||
|
if (item) {
|
||||||
|
const wasEquipped = !!item.slot;
|
||||||
|
item.name = name;
|
||||||
|
item.type = normalizeType(type);
|
||||||
|
item.stats = stats;
|
||||||
|
item.description = description;
|
||||||
|
|
||||||
|
// If type changed and item was equipped, re-equip to valid slot for new type
|
||||||
|
if (wasEquipped) {
|
||||||
|
const newCategory = EQUIPMENT_CATEGORIES[item.type];
|
||||||
|
if (newCategory && !newCategory.slots.includes(item.slot)) {
|
||||||
|
// Old slot is invalid for new type, find a new one
|
||||||
|
const newSlot = findAvailableSlot(item.type, equipment.items.filter(i => i.id !== editId));
|
||||||
|
item.slot = newSlot || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new item
|
||||||
|
const newItem = {
|
||||||
|
id: generateItemId(),
|
||||||
|
name: name,
|
||||||
|
type: normalizeType(type),
|
||||||
|
stats: stats,
|
||||||
|
description: description,
|
||||||
|
slot: null
|
||||||
|
};
|
||||||
|
equipment.items.push(newItem);
|
||||||
|
|
||||||
|
// Auto-equip to first available slot
|
||||||
|
const availableSlot = findAvailableSlot(newItem.type, equipment.items);
|
||||||
|
if (availableSlot) {
|
||||||
|
newItem.slot = availableSlot;
|
||||||
|
} else {
|
||||||
|
console.warn(`[RPG Equipment] Created "${name}" but no available slot for type "${newItem.type}". Item added to inventory unequipped.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEquipmentData();
|
||||||
|
saveSettings();
|
||||||
|
saveChatData();
|
||||||
|
updateMessageSwipeData();
|
||||||
|
closeModal();
|
||||||
|
renderEquipment();
|
||||||
|
renderUserStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equips an item to its designated slot
|
||||||
|
* @param {string} itemId - ID of the item to equip
|
||||||
|
*/
|
||||||
|
export function equipItem(itemId) {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
const item = equipment.items.find(i => i.id === itemId);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const type = normalizeType(item.type);
|
||||||
|
item.type = type;
|
||||||
|
|
||||||
|
const category = EQUIPMENT_CATEGORIES[type];
|
||||||
|
if (!category) return;
|
||||||
|
|
||||||
|
const availableSlot = findAvailableSlot(type, equipment.items);
|
||||||
|
if (!availableSlot) {
|
||||||
|
console.warn(`[RPG Equipment] No available slot for type "${type}". All ${EQUIPMENT_CATEGORIES[type].maxEquipped} slots are full.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.slot = availableSlot;
|
||||||
|
|
||||||
|
updateEquipmentData();
|
||||||
|
saveSettings();
|
||||||
|
saveChatData();
|
||||||
|
updateMessageSwipeData();
|
||||||
|
renderEquipment();
|
||||||
|
renderUserStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unequips an item from a slot
|
||||||
|
* @param {string} slotId - The slot to unequip from
|
||||||
|
*/
|
||||||
|
export function unequipItem(slotId) {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
const item = equipment.items.find(i => i.slot === slotId);
|
||||||
|
if (item) {
|
||||||
|
item.slot = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEquipmentData();
|
||||||
|
saveSettings();
|
||||||
|
saveChatData();
|
||||||
|
updateMessageSwipeData();
|
||||||
|
renderEquipment();
|
||||||
|
renderUserStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an equipment item
|
||||||
|
* @param {string} itemId - ID of the item to delete
|
||||||
|
*/
|
||||||
|
export function deleteItem(itemId) {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
|
||||||
|
// Unequip if currently equipped
|
||||||
|
const item = equipment.items.find(i => i.id === itemId);
|
||||||
|
if (item && item.slot) {
|
||||||
|
item.slot = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from items array
|
||||||
|
equipment.items = equipment.items.filter(i => i.id !== itemId);
|
||||||
|
|
||||||
|
updateEquipmentData();
|
||||||
|
saveSettings();
|
||||||
|
saveChatData();
|
||||||
|
updateMessageSwipeData();
|
||||||
|
renderEquipment();
|
||||||
|
renderUserStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates total equipment bonuses per attribute from currently equipped items
|
||||||
|
* @returns {Object} Map of attribute ID to total bonus value
|
||||||
|
*/
|
||||||
|
export function getEquipmentBonuses() {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
const bonuses = {};
|
||||||
|
const items = equipment.items || [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (!item.slot || !item.stats) continue;
|
||||||
|
|
||||||
|
for (const [attr, val] of Object.entries(item.stats)) {
|
||||||
|
if (val > 0) {
|
||||||
|
bonuses[attr] = (bonuses[attr] || 0) + val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bonuses;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes all event listeners for equipment interactions
|
||||||
|
*/
|
||||||
|
export function initEquipmentEventListeners() {
|
||||||
|
// Show create modal
|
||||||
|
$(document).on('click', '[data-action="show-create-modal"]', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
showCreateModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Equip item from inventory
|
||||||
|
$(document).on('click', '[data-action="equip"]', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const itemId = $(this).data('item-id');
|
||||||
|
equipItem(itemId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unequip item from slot
|
||||||
|
$(document).on('click', '[data-action="unequip"]', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const slot = $(this).closest('.rpg-equipment-slot').data('slot');
|
||||||
|
unequipItem(slot);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit item
|
||||||
|
$(document).on('click', '[data-action="edit-item"]', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const itemId = $(this).data('item-id');
|
||||||
|
showEditModal(itemId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete item
|
||||||
|
$(document).on('click', '[data-action="delete-item"]', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const itemId = $(this).data('item-id');
|
||||||
|
deleteItem(itemId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal close button
|
||||||
|
$(document).on('click', '#rpg-equipment-modal-close', closeModal);
|
||||||
|
|
||||||
|
// Modal cancel button
|
||||||
|
$(document).on('click', '#rpg-equipment-modal-cancel', closeModal);
|
||||||
|
|
||||||
|
// Modal save button
|
||||||
|
$(document).on('click', '#rpg-equipment-modal-save', saveEquipmentItem);
|
||||||
|
|
||||||
|
// Close modal on backdrop click
|
||||||
|
$(document).on('click', '#rpg-equipment-modal', function(e) {
|
||||||
|
if (e.target === this) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter key to save in modal inputs
|
||||||
|
$(document).on('keypress', '#rpg-eq-name', function(e) {
|
||||||
|
if (e.which === 13) {
|
||||||
|
e.preventDefault();
|
||||||
|
saveEquipmentItem();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* Equipment Rendering Module
|
||||||
|
* Handles UI rendering for the equipment grid and item creation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extensionSettings, $equipmentContainer } from '../../core/state.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { EQUIPMENT_CATEGORIES, SLOTS_LIST, escapeHtml } from '../equipment/constants.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a single equipment slot
|
||||||
|
* @param {Object} slotDef - Slot definition from SLOTS_LIST
|
||||||
|
* @param {Object|null} item - The equipped item or null
|
||||||
|
* @returns {string} HTML for the slot
|
||||||
|
*/
|
||||||
|
function renderSlot(slotDef, item) {
|
||||||
|
const slotId = slotDef.id;
|
||||||
|
const slotName = i18n.getTranslation(`equipment.slots.${slotId}`) || slotId.replace(/(\d+)/, ' $1');
|
||||||
|
const equippedClass = item ? 'equipped' : '';
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
const statsText = Object.entries(item.stats || {})
|
||||||
|
.filter(([_, val]) => val > 0)
|
||||||
|
.map(([key, val]) => `<span class="rpg-eq-stat"><span class="rpg-eq-stat-label">${escapeHtml(key.toUpperCase())}</span><span class="rpg-eq-stat-value">+${val}</span></span>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="rpg-equipment-slot ${equippedClass}" data-slot="${slotId}" data-item-id="${item.id}">
|
||||||
|
<div class="rpg-equipment-slot-header">
|
||||||
|
<i class="fa-solid ${slotDef.icon}"></i>
|
||||||
|
<span class="rpg-equipment-slot-name">${escapeHtml(slotName)}</span>
|
||||||
|
<button class="rpg-equipment-unequip-btn" data-action="unequip" title="${i18n.getTranslation('equipment.unequip') || 'Unequip'}">
|
||||||
|
<i class="fa-solid fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-item-name">${escapeHtml(item.name)}</div>
|
||||||
|
${statsText ? `<div class="rpg-equipment-stats">${statsText}</div>` : ''}
|
||||||
|
${item.description ? `<div class="rpg-equipment-description">${escapeHtml(item.description)}</div>` : ''}
|
||||||
|
<div class="rpg-equipment-item-actions">
|
||||||
|
<button class="rpg-equipment-edit-btn" data-action="edit-item" data-item-id="${item.id}" title="${i18n.getTranslation('equipment.editItem') || 'Edit item'}">
|
||||||
|
<i class="fa-solid fa-pen"></i>
|
||||||
|
</button>
|
||||||
|
<button class="rpg-equipment-delete-btn" data-action="delete-item" data-item-id="${item.id}" title="${i18n.getTranslation('equipment.deleteItem') || 'Delete item'}">
|
||||||
|
<i class="fa-solid fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="rpg-equipment-slot" data-slot="${slotId}">
|
||||||
|
<div class="rpg-equipment-slot-header">
|
||||||
|
<i class="fa-solid ${slotDef.icon}"></i>
|
||||||
|
<span class="rpg-equipment-slot-name">${escapeHtml(slotName)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-empty">${i18n.getTranslation('equipment.emptySlot') || 'Empty'}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the full equipment section HTML
|
||||||
|
* @returns {string} Complete HTML for the equipment section
|
||||||
|
*/
|
||||||
|
function generateEquipmentHTML() {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
const slots = equipment.slots || {};
|
||||||
|
const items = equipment.items || [];
|
||||||
|
|
||||||
|
let html = '<div class="rpg-equipment-container">';
|
||||||
|
|
||||||
|
// Header with add button
|
||||||
|
html += `
|
||||||
|
<div class="rpg-equipment-header">
|
||||||
|
<h3>
|
||||||
|
<i class="fa-solid fa-shield-halved"></i>
|
||||||
|
<span data-i18n-key="equipment.title">${i18n.getTranslation('equipment.title') || 'Equipment'}</span>
|
||||||
|
</h3>
|
||||||
|
<button class="rpg-equipment-add-btn" data-action="show-create-modal" title="${i18n.getTranslation('equipment.createItem') || 'Create new equipment'}">
|
||||||
|
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('equipment.createItem') || 'Create Equipment'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Equipment grid
|
||||||
|
html += '<div class="rpg-equipment-grid">';
|
||||||
|
|
||||||
|
// Render each slot
|
||||||
|
for (const slotDef of SLOTS_LIST) {
|
||||||
|
const item = items.find(i => i.slot === slotDef.id);
|
||||||
|
html += renderSlot(slotDef, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Inventory list (items not currently equipped)
|
||||||
|
const unequipped = items.filter(item => !item.slot);
|
||||||
|
if (unequipped.length > 0) {
|
||||||
|
html += '<div class="rpg-equipment-inventory">';
|
||||||
|
html += `<h4>${i18n.getTranslation('equipment.inventoryTitle') || 'Inventory'}</h4>`;
|
||||||
|
html += '<div class="rpg-equipment-inventory-list">';
|
||||||
|
|
||||||
|
for (const item of unequipped) {
|
||||||
|
const category = EQUIPMENT_CATEGORIES[item.type];
|
||||||
|
const statsText = Object.entries(item.stats || {})
|
||||||
|
.filter(([_, val]) => val > 0)
|
||||||
|
.map(([key, val]) => `<span class="rpg-eq-stat"><span class="rpg-eq-stat-label">${escapeHtml(key.toUpperCase())}</span><span class="rpg-eq-stat-value">+${val}</span></span>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="rpg-equipment-inventory-item" data-item-id="${item.id}">
|
||||||
|
<div class="rpg-equipment-inventory-item-header">
|
||||||
|
<i class="fa-solid ${category ? category.icon : 'fa-circle'}"></i>
|
||||||
|
<span class="rpg-equipment-inventory-item-name">${escapeHtml(item.name)}</span>
|
||||||
|
<span class="rpg-equipment-inventory-item-type">${category ? (i18n.getTranslation(`equipment.types.${item.type}`) || item.type) : escapeHtml(item.type)}</span>
|
||||||
|
</div>
|
||||||
|
${statsText ? `<div class="rpg-equipment-stats">${statsText}</div>` : ''}
|
||||||
|
<div class="rpg-equipment-item-actions">
|
||||||
|
<button class="rpg-equipment-equip-btn" data-action="equip" data-item-id="${item.id}" title="${i18n.getTranslation('equipment.equip') || 'Equip'}">
|
||||||
|
<i class="fa-solid fa-hand"></i>
|
||||||
|
</button>
|
||||||
|
<button class="rpg-equipment-edit-btn" data-action="edit-item" data-item-id="${item.id}" title="${i18n.getTranslation('equipment.editItem') || 'Edit item'}">
|
||||||
|
<i class="fa-solid fa-pen"></i>
|
||||||
|
</button>
|
||||||
|
<button class="rpg-equipment-delete-btn" data-action="delete-item" data-item-id="${item.id}" title="${i18n.getTranslation('equipment.deleteItem') || 'Delete item'}">
|
||||||
|
<i class="fa-solid fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main equipment rendering function
|
||||||
|
* Gets data from state/settings and updates DOM directly.
|
||||||
|
*/
|
||||||
|
export function renderEquipment() {
|
||||||
|
if (!$equipmentContainer || !extensionSettings.showEquipment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = generateEquipmentHTML();
|
||||||
|
$equipmentContainer.html(html);
|
||||||
|
|
||||||
|
// Re-apply translations
|
||||||
|
i18n.applyTranslations($equipmentContainer[0]);
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import { buildInventorySummary } from '../generation/promptBuilder.js';
|
|||||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||||
import { updateFabWidgets } from '../ui/mobile.js';
|
import { updateFabWidgets } from '../ui/mobile.js';
|
||||||
import { getStatBarColors } from '../ui/theme.js';
|
import { getStatBarColors } from '../ui/theme.js';
|
||||||
|
import { getEquipmentBonuses } from '../interaction/equipmentActions.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
|
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
|
||||||
@@ -410,6 +411,7 @@ export function renderUserStats() {
|
|||||||
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
|
const enabledAttributes = rpgAttributes.filter(attr => attr && attr.enabled && attr.name && attr.id);
|
||||||
|
|
||||||
if (enabledAttributes.length > 0) {
|
if (enabledAttributes.length > 0) {
|
||||||
|
const equipmentBonuses = getEquipmentBonuses();
|
||||||
html += `
|
html += `
|
||||||
<div class="rpg-stats-right">
|
<div class="rpg-stats-right">
|
||||||
<div class="rpg-classic-stats">
|
<div class="rpg-classic-stats">
|
||||||
@@ -418,12 +420,14 @@ export function renderUserStats() {
|
|||||||
|
|
||||||
enabledAttributes.forEach(attr => {
|
enabledAttributes.forEach(attr => {
|
||||||
const value = extensionSettings.classicStats[attr.id] !== undefined ? extensionSettings.classicStats[attr.id] : 10;
|
const value = extensionSettings.classicStats[attr.id] !== undefined ? extensionSettings.classicStats[attr.id] : 10;
|
||||||
|
const bonus = equipmentBonuses[attr.id] || 0;
|
||||||
|
const bonusHtml = bonus > 0 ? `<span class="rpg-classic-stat-bonus" title="Equipment bonus: +${bonus}"> +${bonus}</span>` : '';
|
||||||
html += `
|
html += `
|
||||||
<div class="rpg-classic-stat" data-stat="${attr.id}">
|
<div class="rpg-classic-stat" data-stat="${attr.id}">
|
||||||
<span class="rpg-classic-stat-label">${attr.name}</span>
|
<span class="rpg-classic-stat-label">${attr.name}</span>
|
||||||
<div class="rpg-classic-stat-buttons">
|
<div class="rpg-classic-stat-buttons">
|
||||||
<button class="rpg-classic-stat-btn rpg-stat-decrease" data-stat="${attr.id}">−</button>
|
<button class="rpg-classic-stat-btn rpg-stat-decrease" data-stat="${attr.id}">−</button>
|
||||||
<span class="rpg-classic-stat-value">${value}</span>
|
<span class="rpg-classic-stat-value">${value}${bonusHtml}</span>
|
||||||
<button class="rpg-classic-stat-btn rpg-stat-increase" data-stat="${attr.id}">+</button>
|
<button class="rpg-classic-stat-btn rpg-stat-increase" data-stat="${attr.id}">+</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -292,16 +292,18 @@ export function setupDesktopTabs() {
|
|||||||
const $infoBox = $('#rpg-info-box');
|
const $infoBox = $('#rpg-info-box');
|
||||||
const $thoughts = $('#rpg-thoughts');
|
const $thoughts = $('#rpg-thoughts');
|
||||||
const $inventory = $('#rpg-inventory');
|
const $inventory = $('#rpg-inventory');
|
||||||
|
const $equipment = $('#rpg-equipment');
|
||||||
const $quests = $('#rpg-quests');
|
const $quests = $('#rpg-quests');
|
||||||
|
|
||||||
// If no sections exist, nothing to organize
|
// If no sections exist, nothing to organize
|
||||||
if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0 && $inventory.length === 0 && $quests.length === 0) {
|
if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0 && $inventory.length === 0 && $equipment.length === 0 && $quests.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build tab navigation dynamically based on enabled settings
|
// Build tab navigation dynamically based on enabled settings
|
||||||
const tabButtons = [];
|
const tabButtons = [];
|
||||||
const hasInventory = $inventory.length > 0 && extensionSettings.showInventory;
|
const hasInventory = $inventory.length > 0 && extensionSettings.showInventory;
|
||||||
|
const hasEquipment = $equipment.length > 0 && extensionSettings.showEquipment;
|
||||||
const hasQuests = $quests.length > 0 && extensionSettings.showQuests;
|
const hasQuests = $quests.length > 0 && extensionSettings.showQuests;
|
||||||
|
|
||||||
// Status tab (always present if any status content exists)
|
// Status tab (always present if any status content exists)
|
||||||
@@ -322,6 +324,16 @@ export function setupDesktopTabs() {
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equipment tab (only if enabled in settings)
|
||||||
|
if (hasEquipment) {
|
||||||
|
tabButtons.push(`
|
||||||
|
<button class="rpg-tab-btn" data-tab="equipment">
|
||||||
|
<i class="fa-solid fa-shield-halved"></i>
|
||||||
|
<span data-i18n-key="equipment.title">Equipment</span>
|
||||||
|
</button>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
// Quests tab (only if enabled in settings)
|
// Quests tab (only if enabled in settings)
|
||||||
if (hasQuests) {
|
if (hasQuests) {
|
||||||
tabButtons.push(`
|
tabButtons.push(`
|
||||||
@@ -337,6 +349,7 @@ export function setupDesktopTabs() {
|
|||||||
// Create tab content containers
|
// Create tab content containers
|
||||||
const $statusTab = $('<div class="rpg-tab-content active" data-tab-content="status"></div>');
|
const $statusTab = $('<div class="rpg-tab-content active" data-tab-content="status"></div>');
|
||||||
const $inventoryTab = $('<div class="rpg-tab-content" data-tab-content="inventory"></div>');
|
const $inventoryTab = $('<div class="rpg-tab-content" data-tab-content="inventory"></div>');
|
||||||
|
const $equipmentTab = $('<div class="rpg-tab-content" data-tab-content="equipment"></div>');
|
||||||
const $questsTab = $('<div class="rpg-tab-content" data-tab-content="quests"></div>');
|
const $questsTab = $('<div class="rpg-tab-content" data-tab-content="quests"></div>');
|
||||||
|
|
||||||
// Move sections into their respective tabs (detach to preserve event handlers)
|
// Move sections into their respective tabs (detach to preserve event handlers)
|
||||||
@@ -361,6 +374,11 @@ export function setupDesktopTabs() {
|
|||||||
// Only show if enabled (will be part of tab structure)
|
// Only show if enabled (will be part of tab structure)
|
||||||
if (hasInventory) $inventory.show();
|
if (hasInventory) $inventory.show();
|
||||||
}
|
}
|
||||||
|
if ($equipment.length > 0) {
|
||||||
|
$equipmentTab.append($equipment.detach());
|
||||||
|
// Only show if enabled (will be part of tab structure)
|
||||||
|
if (hasEquipment) $equipment.show();
|
||||||
|
}
|
||||||
if ($quests.length > 0) {
|
if ($quests.length > 0) {
|
||||||
$questsTab.append($quests.detach());
|
$questsTab.append($quests.detach());
|
||||||
// Only show if enabled (will be part of tab structure)
|
// Only show if enabled (will be part of tab structure)
|
||||||
@@ -378,6 +396,7 @@ export function setupDesktopTabs() {
|
|||||||
// Always append inventory and quests tabs to preserve the elements
|
// Always append inventory and quests tabs to preserve the elements
|
||||||
// But they'll only show if enabled (via tab button visibility)
|
// But they'll only show if enabled (via tab button visibility)
|
||||||
$tabsContainer.append($inventoryTab);
|
$tabsContainer.append($inventoryTab);
|
||||||
|
$tabsContainer.append($equipmentTab);
|
||||||
$tabsContainer.append($questsTab);
|
$tabsContainer.append($questsTab);
|
||||||
|
|
||||||
// Replace content box with tabs container
|
// Replace content box with tabs container
|
||||||
@@ -410,6 +429,7 @@ export function removeDesktopTabs() {
|
|||||||
const $infoBox = $('#rpg-info-box').detach();
|
const $infoBox = $('#rpg-info-box').detach();
|
||||||
const $thoughts = $('#rpg-thoughts').detach();
|
const $thoughts = $('#rpg-thoughts').detach();
|
||||||
const $inventory = $('#rpg-inventory').detach();
|
const $inventory = $('#rpg-inventory').detach();
|
||||||
|
const $equipment = $('#rpg-equipment').detach();
|
||||||
const $quests = $('#rpg-quests').detach();
|
const $quests = $('#rpg-quests').detach();
|
||||||
|
|
||||||
// Remove tabs container
|
// Remove tabs container
|
||||||
@@ -419,16 +439,19 @@ export function removeDesktopTabs() {
|
|||||||
const $dividerStats = $('#rpg-divider-stats');
|
const $dividerStats = $('#rpg-divider-stats');
|
||||||
const $dividerInfo = $('#rpg-divider-info');
|
const $dividerInfo = $('#rpg-divider-info');
|
||||||
const $dividerThoughts = $('#rpg-divider-thoughts');
|
const $dividerThoughts = $('#rpg-divider-thoughts');
|
||||||
|
const $dividerInventory = $('#rpg-divider-inventory');
|
||||||
|
const $dividerEquipment = $('#rpg-divider-equipment');
|
||||||
|
|
||||||
// Restore original sections to content box in correct order
|
// Restore original sections to content box in correct order
|
||||||
const $contentBox = $('.rpg-content-box');
|
const $contentBox = $('.rpg-content-box');
|
||||||
|
|
||||||
// Re-insert sections in original order: User Stats, Info Box, Thoughts, Inventory, Quests
|
// Re-insert sections in original order: User Stats, Info Box, Thoughts, Inventory, Equipment, Quests
|
||||||
if ($dividerStats.length) {
|
if ($dividerStats.length) {
|
||||||
$dividerStats.before($userStats);
|
$dividerStats.before($userStats);
|
||||||
$dividerInfo.before($infoBox);
|
$dividerInfo.before($infoBox);
|
||||||
$dividerThoughts.before($thoughts);
|
$dividerThoughts.before($thoughts);
|
||||||
$contentBox.append($inventory);
|
$dividerInventory.before($inventory);
|
||||||
|
$dividerEquipment.before($equipment);
|
||||||
$contentBox.append($quests);
|
$contentBox.append($quests);
|
||||||
} else {
|
} else {
|
||||||
// Fallback if dividers don't exist
|
// Fallback if dividers don't exist
|
||||||
@@ -436,6 +459,7 @@ export function removeDesktopTabs() {
|
|||||||
$contentBox.append($infoBox);
|
$contentBox.append($infoBox);
|
||||||
$contentBox.append($thoughts);
|
$contentBox.append($thoughts);
|
||||||
$contentBox.append($inventory);
|
$contentBox.append($inventory);
|
||||||
|
$contentBox.append($equipment);
|
||||||
$contentBox.append($quests);
|
$contentBox.append($quests);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,6 +471,7 @@ export function removeDesktopTabs() {
|
|||||||
}
|
}
|
||||||
if (extensionSettings.showCharacterThoughts) $thoughts.show();
|
if (extensionSettings.showCharacterThoughts) $thoughts.show();
|
||||||
if (extensionSettings.showInventory) $inventory.show();
|
if (extensionSettings.showInventory) $inventory.show();
|
||||||
|
if (extensionSettings.showEquipment) $equipment.show();
|
||||||
if (extensionSettings.showQuests) $quests.show();
|
if (extensionSettings.showQuests) $quests.show();
|
||||||
$('.rpg-divider').show();
|
$('.rpg-divider').show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -317,6 +317,12 @@ export function updateSectionVisibility() {
|
|||||||
$('#rpg-quests').hide();
|
$('#rpg-quests').hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (extensionSettings.showEquipment) {
|
||||||
|
$('#rpg-equipment').show();
|
||||||
|
} else {
|
||||||
|
$('#rpg-equipment').hide();
|
||||||
|
}
|
||||||
|
|
||||||
if ($musicPlayerContainer) {
|
if ($musicPlayerContainer) {
|
||||||
if (extensionSettings.enableSpotifyMusic) {
|
if (extensionSettings.enableSpotifyMusic) {
|
||||||
$musicPlayerContainer.show();
|
$musicPlayerContainer.show();
|
||||||
@@ -328,7 +334,7 @@ export function updateSectionVisibility() {
|
|||||||
// Show/hide dividers intelligently
|
// Show/hide dividers intelligently
|
||||||
// Divider after User Stats: shown if User Stats is visible AND at least one section after it is visible
|
// Divider after User Stats: shown if User Stats is visible AND at least one section after it is visible
|
||||||
const showDividerAfterStats = extensionSettings.showUserStats &&
|
const showDividerAfterStats = extensionSettings.showUserStats &&
|
||||||
(extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
(extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showEquipment || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
||||||
if (showDividerAfterStats) {
|
if (showDividerAfterStats) {
|
||||||
$('#rpg-divider-stats').show();
|
$('#rpg-divider-stats').show();
|
||||||
} else {
|
} else {
|
||||||
@@ -337,7 +343,7 @@ export function updateSectionVisibility() {
|
|||||||
|
|
||||||
// Divider after Info Box: shown if Info Box is visible AND at least one section after it is visible
|
// Divider after Info Box: shown if Info Box is visible AND at least one section after it is visible
|
||||||
const showDividerAfterInfo = extensionSettings.showInfoBox &&
|
const showDividerAfterInfo = extensionSettings.showInfoBox &&
|
||||||
(extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showQuests);
|
(extensionSettings.showCharacterThoughts || extensionSettings.showInventory || extensionSettings.showEquipment || extensionSettings.showQuests);
|
||||||
if (showDividerAfterInfo) {
|
if (showDividerAfterInfo) {
|
||||||
$('#rpg-divider-info').show();
|
$('#rpg-divider-info').show();
|
||||||
} else {
|
} else {
|
||||||
@@ -346,21 +352,29 @@ export function updateSectionVisibility() {
|
|||||||
|
|
||||||
// Divider after Thoughts: shown if Thoughts is visible AND at least one section after it is visible
|
// Divider after Thoughts: shown if Thoughts is visible AND at least one section after it is visible
|
||||||
const showDividerAfterThoughts = extensionSettings.showCharacterThoughts &&
|
const showDividerAfterThoughts = extensionSettings.showCharacterThoughts &&
|
||||||
(extensionSettings.showInventory || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
(extensionSettings.showInventory || extensionSettings.showEquipment || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
||||||
if (showDividerAfterThoughts) {
|
if (showDividerAfterThoughts) {
|
||||||
$('#rpg-divider-thoughts').show();
|
$('#rpg-divider-thoughts').show();
|
||||||
} else {
|
} else {
|
||||||
$('#rpg-divider-thoughts').hide();
|
$('#rpg-divider-thoughts').hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Divider after Inventory: shown if Inventory is visible AND (Quests or Music) is visible
|
// Divider after Inventory: shown if Inventory is visible AND (Equipment, Quests or Music) is visible
|
||||||
const showDividerAfterInventory = extensionSettings.showInventory && (extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
const showDividerAfterInventory = extensionSettings.showInventory && (extensionSettings.showEquipment || extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
||||||
if (showDividerAfterInventory) {
|
if (showDividerAfterInventory) {
|
||||||
$('#rpg-divider-inventory').show();
|
$('#rpg-divider-inventory').show();
|
||||||
} else {
|
} else {
|
||||||
$('#rpg-divider-inventory').hide();
|
$('#rpg-divider-inventory').hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Divider after Equipment: shown if Equipment is visible AND (Quests or Music) is visible
|
||||||
|
const showDividerAfterEquipment = extensionSettings.showEquipment && (extensionSettings.showQuests || extensionSettings.enableSpotifyMusic);
|
||||||
|
if (showDividerAfterEquipment) {
|
||||||
|
$('#rpg-divider-equipment').show();
|
||||||
|
} else {
|
||||||
|
$('#rpg-divider-equipment').hide();
|
||||||
|
}
|
||||||
|
|
||||||
// Divider after Quests: shown if Quests is visible AND Music is visible
|
// Divider after Quests: shown if Quests is visible AND Music is visible
|
||||||
const showDividerAfterQuests = extensionSettings.showQuests && extensionSettings.enableSpotifyMusic;
|
const showDividerAfterQuests = extensionSettings.showQuests && extensionSettings.enableSpotifyMusic;
|
||||||
if (showDividerAfterQuests) {
|
if (showDividerAfterQuests) {
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ export function updateMobileTabLabels() {
|
|||||||
case 'inventory':
|
case 'inventory':
|
||||||
translationKey = 'global.inventory';
|
translationKey = 'global.inventory';
|
||||||
break;
|
break;
|
||||||
|
case 'equipment':
|
||||||
|
translationKey = 'equipment.title';
|
||||||
|
break;
|
||||||
case 'quests':
|
case 'quests':
|
||||||
translationKey = 'global.quests';
|
translationKey = 'global.quests';
|
||||||
break;
|
break;
|
||||||
@@ -49,6 +52,9 @@ export function updateMobileTabLabels() {
|
|||||||
case 'inventory':
|
case 'inventory':
|
||||||
fallback = 'Inventory';
|
fallback = 'Inventory';
|
||||||
break;
|
break;
|
||||||
|
case 'equipment':
|
||||||
|
fallback = 'Equipment';
|
||||||
|
break;
|
||||||
case 'quests':
|
case 'quests':
|
||||||
fallback = 'Quests';
|
fallback = 'Quests';
|
||||||
break;
|
break;
|
||||||
@@ -606,10 +612,11 @@ export function setupMobileTabs() {
|
|||||||
const $infoBox = $('#rpg-info-box');
|
const $infoBox = $('#rpg-info-box');
|
||||||
const $thoughts = $('#rpg-thoughts');
|
const $thoughts = $('#rpg-thoughts');
|
||||||
const $inventory = $('#rpg-inventory');
|
const $inventory = $('#rpg-inventory');
|
||||||
|
const $equipment = $('#rpg-equipment');
|
||||||
const $quests = $('#rpg-quests');
|
const $quests = $('#rpg-quests');
|
||||||
|
|
||||||
// If no sections exist, nothing to organize
|
// If no sections exist, nothing to organize
|
||||||
if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0 && $inventory.length === 0 && $quests.length === 0) {
|
if ($userStats.length === 0 && $infoBox.length === 0 && $thoughts.length === 0 && $inventory.length === 0 && $equipment.length === 0 && $quests.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,6 +625,7 @@ export function setupMobileTabs() {
|
|||||||
const hasStats = $userStats.length > 0;
|
const hasStats = $userStats.length > 0;
|
||||||
const hasInfo = $infoBox.length > 0 || $thoughts.length > 0;
|
const hasInfo = $infoBox.length > 0 || $thoughts.length > 0;
|
||||||
const hasInventory = $inventory.length > 0 && extensionSettings.showInventory;
|
const hasInventory = $inventory.length > 0 && extensionSettings.showInventory;
|
||||||
|
const hasEquipment = $equipment.length > 0 && extensionSettings.showEquipment;
|
||||||
const hasQuests = $quests.length > 0 && extensionSettings.showQuests;
|
const hasQuests = $quests.length > 0 && extensionSettings.showQuests;
|
||||||
|
|
||||||
// Tab 1: Stats (User Stats only)
|
// Tab 1: Stats (User Stats only)
|
||||||
@@ -632,6 +640,10 @@ export function setupMobileTabs() {
|
|||||||
if (hasInventory) {
|
if (hasInventory) {
|
||||||
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="inventory"><i class="fa-solid fa-box"></i><span>' + (i18n.getTranslation('global.inventory') || 'Inventory') + '</span></button>');
|
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="inventory"><i class="fa-solid fa-box"></i><span>' + (i18n.getTranslation('global.inventory') || 'Inventory') + '</span></button>');
|
||||||
}
|
}
|
||||||
|
// Tab 3.5: Equipment
|
||||||
|
if (hasEquipment) {
|
||||||
|
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="equipment"><i class="fa-solid fa-shield-halved"></i><span>' + (i18n.getTranslation('equipment.title') || 'Equipment') + '</span></button>');
|
||||||
|
}
|
||||||
// Tab 4: Quests
|
// Tab 4: Quests
|
||||||
if (hasQuests) {
|
if (hasQuests) {
|
||||||
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="quests"><i class="fa-solid fa-scroll"></i><span>' + (i18n.getTranslation('global.quests') || 'Quests') + '</span></button>');
|
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="quests"><i class="fa-solid fa-scroll"></i><span>' + (i18n.getTranslation('global.quests') || 'Quests') + '</span></button>');
|
||||||
@@ -644,12 +656,14 @@ export function setupMobileTabs() {
|
|||||||
if (hasStats) firstTab = 'stats';
|
if (hasStats) firstTab = 'stats';
|
||||||
else if (hasInfo) firstTab = 'info';
|
else if (hasInfo) firstTab = 'info';
|
||||||
else if (hasInventory) firstTab = 'inventory';
|
else if (hasInventory) firstTab = 'inventory';
|
||||||
|
else if (hasEquipment) firstTab = 'equipment';
|
||||||
else if (hasQuests) firstTab = 'quests';
|
else if (hasQuests) firstTab = 'quests';
|
||||||
|
|
||||||
// Create tab content wrappers
|
// Create tab content wrappers
|
||||||
const $statsTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'stats' ? 'active' : '') + '" data-tab-content="stats"></div>');
|
const $statsTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'stats' ? 'active' : '') + '" data-tab-content="stats"></div>');
|
||||||
const $infoTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'info' ? 'active' : '') + '" data-tab-content="info"></div>');
|
const $infoTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'info' ? 'active' : '') + '" data-tab-content="info"></div>');
|
||||||
const $inventoryTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'inventory' ? 'active' : '') + '" data-tab-content="inventory"></div>');
|
const $inventoryTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'inventory' ? 'active' : '') + '" data-tab-content="inventory"></div>');
|
||||||
|
const $equipmentTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'equipment' ? 'active' : '') + '" data-tab-content="equipment"></div>');
|
||||||
const $questsTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'quests' ? 'active' : '') + '" data-tab-content="quests"></div>');
|
const $questsTab = $('<div class="rpg-mobile-tab-content ' + (firstTab === 'quests' ? 'active' : '') + '" data-tab-content="quests"></div>');
|
||||||
|
|
||||||
// Move sections into their respective tabs (detach to preserve event handlers)
|
// Move sections into their respective tabs (detach to preserve event handlers)
|
||||||
@@ -677,6 +691,12 @@ export function setupMobileTabs() {
|
|||||||
$inventory.show();
|
$inventory.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equipment tab: Equipment only
|
||||||
|
if ($equipment.length > 0) {
|
||||||
|
$equipmentTab.append($equipment.detach());
|
||||||
|
$equipment.show();
|
||||||
|
}
|
||||||
|
|
||||||
// Quests tab: Quests only
|
// Quests tab: Quests only
|
||||||
if ($quests.length > 0) {
|
if ($quests.length > 0) {
|
||||||
$questsTab.append($quests.detach());
|
$questsTab.append($quests.detach());
|
||||||
@@ -695,6 +715,7 @@ export function setupMobileTabs() {
|
|||||||
$mobileContainer.append($statsTab);
|
$mobileContainer.append($statsTab);
|
||||||
$mobileContainer.append($infoTab);
|
$mobileContainer.append($infoTab);
|
||||||
$mobileContainer.append($inventoryTab);
|
$mobileContainer.append($inventoryTab);
|
||||||
|
$mobileContainer.append($equipmentTab);
|
||||||
$mobileContainer.append($questsTab);
|
$mobileContainer.append($questsTab);
|
||||||
|
|
||||||
// Insert mobile tab structure at the beginning of content box
|
// Insert mobile tab structure at the beginning of content box
|
||||||
@@ -723,6 +744,7 @@ export function removeMobileTabs() {
|
|||||||
const $infoBox = $('#rpg-info-box').detach();
|
const $infoBox = $('#rpg-info-box').detach();
|
||||||
const $thoughts = $('#rpg-thoughts').detach();
|
const $thoughts = $('#rpg-thoughts').detach();
|
||||||
const $inventory = $('#rpg-inventory').detach();
|
const $inventory = $('#rpg-inventory').detach();
|
||||||
|
const $equipment = $('#rpg-equipment').detach();
|
||||||
const $quests = $('#rpg-quests').detach();
|
const $quests = $('#rpg-quests').detach();
|
||||||
|
|
||||||
// Remove mobile tab container
|
// Remove mobile tab container
|
||||||
@@ -732,20 +754,24 @@ export function removeMobileTabs() {
|
|||||||
const $dividerStats = $('#rpg-divider-stats');
|
const $dividerStats = $('#rpg-divider-stats');
|
||||||
const $dividerInfo = $('#rpg-divider-info');
|
const $dividerInfo = $('#rpg-divider-info');
|
||||||
const $dividerThoughts = $('#rpg-divider-thoughts');
|
const $dividerThoughts = $('#rpg-divider-thoughts');
|
||||||
|
const $dividerInventory = $('#rpg-divider-inventory');
|
||||||
|
const $dividerEquipment = $('#rpg-divider-equipment');
|
||||||
|
|
||||||
// Restore original sections to content box in correct order
|
// Restore original sections to content box in correct order
|
||||||
const $contentBox = $('.rpg-content-box');
|
const $contentBox = $('.rpg-content-box');
|
||||||
|
|
||||||
// Re-insert sections in original order: User Stats, Info Box, Thoughts, Inventory, Quests
|
// Re-insert sections in original order: User Stats, Info Box, Thoughts, Inventory, Equipment, Quests
|
||||||
if ($dividerStats.length) {
|
if ($dividerStats.length) {
|
||||||
$dividerStats.before($userStats);
|
$dividerStats.before($userStats);
|
||||||
$dividerInfo.before($infoBox);
|
$dividerInfo.before($infoBox);
|
||||||
$dividerThoughts.before($thoughts);
|
$dividerThoughts.before($thoughts);
|
||||||
$contentBox.append($inventory);
|
$dividerInventory.before($inventory);
|
||||||
|
$dividerEquipment.before($equipment);
|
||||||
$contentBox.append($quests);
|
$contentBox.append($quests);
|
||||||
} else {
|
} else {
|
||||||
// Fallback if dividers don't exist
|
// Fallback if dividers don't exist
|
||||||
$contentBox.prepend($quests);
|
$contentBox.prepend($quests);
|
||||||
|
$contentBox.prepend($equipment);
|
||||||
$contentBox.prepend($inventory);
|
$contentBox.prepend($inventory);
|
||||||
$contentBox.prepend($thoughts);
|
$contentBox.prepend($thoughts);
|
||||||
$contentBox.prepend($infoBox);
|
$contentBox.prepend($infoBox);
|
||||||
@@ -760,6 +786,7 @@ export function removeMobileTabs() {
|
|||||||
}
|
}
|
||||||
if (extensionSettings.showCharacterThoughts) $thoughts.show();
|
if (extensionSettings.showCharacterThoughts) $thoughts.show();
|
||||||
if (extensionSettings.showInventory) $inventory.show();
|
if (extensionSettings.showInventory) $inventory.show();
|
||||||
|
if (extensionSettings.showEquipment) $equipment.show();
|
||||||
if (extensionSettings.showQuests) $quests.show();
|
if (extensionSettings.showQuests) $quests.show();
|
||||||
$('.rpg-divider').show();
|
$('.rpg-divider').show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { renderInfoBox } from '../rendering/infoBox.js';
|
|||||||
import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js';
|
import { renderThoughts, updateChatThoughts } from '../rendering/thoughts.js';
|
||||||
import { renderQuests } from '../rendering/quests.js';
|
import { renderQuests } from '../rendering/quests.js';
|
||||||
import { renderInventory } from '../rendering/inventory.js';
|
import { renderInventory } from '../rendering/inventory.js';
|
||||||
|
import { renderEquipment } from '../rendering/equipment.js';
|
||||||
import {
|
import {
|
||||||
rollDice as rollDiceCore,
|
rollDice as rollDiceCore,
|
||||||
clearDiceRoll as clearDiceRollCore,
|
clearDiceRoll as clearDiceRollCore,
|
||||||
@@ -503,6 +504,7 @@ export function setupSettingsPopup() {
|
|||||||
updateDiceDisplayCore();
|
updateDiceDisplayCore();
|
||||||
updateChatThoughts();
|
updateChatThoughts();
|
||||||
renderInventory();
|
renderInventory();
|
||||||
|
renderEquipment();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
|
|
||||||
// console.log('[RPG Companion] Cache cleared successfully');
|
// console.log('[RPG Companion] Cache cleared successfully');
|
||||||
|
|||||||
@@ -11908,3 +11908,389 @@ body.documentstyle .rpg-inline-thoughts {
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Equipment Section Styles
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.rpg-equipment-section {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-container {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--SmartThemeTitleColor);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-add-btn {
|
||||||
|
background: var(--SmartThemeAccentColor);
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-add-btn:hover {
|
||||||
|
background: var(--SmartThemeButtonColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-slot {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
min-height: 70px;
|
||||||
|
transition: border-color 0.2s, background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-slot.equipped {
|
||||||
|
border-color: var(--SmartThemeAccentColor);
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-slot-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--SmartThemeTitleColor);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-slot-header i {
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-slot-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-unequip-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px;
|
||||||
|
font-size: 9px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-unequip-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-item-name {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--SmartThemeTitleColor);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-empty {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
opacity: 0.4;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 3px;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-eq-stat {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 9px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-eq-stat-label {
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-eq-stat-value {
|
||||||
|
color: #4caf50;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-description {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-top: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding-top: 4px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-edit-btn,
|
||||||
|
.rpg-equipment-delete-btn,
|
||||||
|
.rpg-equipment-equip-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
transition: opacity 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-edit-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: #4fc3f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-delete-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-equip-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Equipment Inventory (unequipped items) */
|
||||||
|
.rpg-equipment-inventory {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-inventory h4 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--SmartThemeTitleColor);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-inventory-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-inventory-item {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-inventory-item:hover {
|
||||||
|
border-color: var(--SmartThemeAccentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-inventory-item-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-inventory-item-header i {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-inventory-item-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--SmartThemeTitleColor);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-inventory-item-type {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
opacity: 0.5;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Equipment Modal */
|
||||||
|
.rpg-equipment-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-modal-content {
|
||||||
|
background: var(--SmartThemeDialogBgColor);
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 420px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--SmartThemeTitleColor);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-modal-body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-form-group {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--SmartThemeTitleColor);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-form-group .rpg-input,
|
||||||
|
.rpg-equipment-form-group .rpg-select,
|
||||||
|
.rpg-equipment-form-group .rpg-textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--SmartThemeInputBg);
|
||||||
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-form-group .rpg-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-eq-stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-eq-stat-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-eq-stat-checkbox input[type="checkbox"] {
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-eq-stat-check-label {
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-eq-stat-value-input {
|
||||||
|
width: 30px;
|
||||||
|
padding: 1px 2px;
|
||||||
|
background: var(--SmartThemeInputBg);
|
||||||
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
border-radius: 2px;
|
||||||
|
color: var(--SmartThemeBodyColor);
|
||||||
|
font-size: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpg-equipment-modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-top: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Equipment bonus display on RPG attributes */
|
||||||
|
.rpg-classic-stat-bonus {
|
||||||
|
color: #4caf50;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -96,6 +96,14 @@
|
|||||||
<!-- Divider after Inventory -->
|
<!-- Divider after Inventory -->
|
||||||
<div id="rpg-divider-inventory" class="rpg-divider"></div>
|
<div id="rpg-divider-inventory" class="rpg-divider"></div>
|
||||||
|
|
||||||
|
<!-- Equipment Section -->
|
||||||
|
<div id="rpg-equipment" class="rpg-section rpg-equipment-section">
|
||||||
|
<!-- Content will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Divider after Equipment -->
|
||||||
|
<div id="rpg-divider-equipment" class="rpg-divider"></div>
|
||||||
|
|
||||||
<!-- Quests Section -->
|
<!-- Quests Section -->
|
||||||
<div id="rpg-quests" class="rpg-section rpg-quests-section">
|
<div id="rpg-quests" class="rpg-section rpg-quests-section">
|
||||||
<!-- Content will be populated by JavaScript -->
|
<!-- Content will be populated by JavaScript -->
|
||||||
@@ -448,6 +456,15 @@
|
|||||||
Track items carried, clothing worn, stored items, and assets.
|
Track items carried, clothing worn, stored items, and assets.
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-equipment" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.showEquipment">Show Equipment</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.showEquipmentNote">
|
||||||
|
Manage equipped gear and stat bonuses from items.
|
||||||
|
</small>
|
||||||
|
|
||||||
<label class="checkbox_label">
|
<label class="checkbox_label">
|
||||||
<input type="checkbox" id="rpg-toggle-quests" />
|
<input type="checkbox" id="rpg-toggle-quests" />
|
||||||
<span data-i18n-key="template.settingsModal.display.showQuests">Show Quests</span>
|
<span data-i18n-key="template.settingsModal.display.showQuests">Show Quests</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user