Compare commits

...

7 Commits

Author SHA1 Message Date
ARIA 54e1b0c2b2 Fix equipment vanishing after save: preserve equipment in loadChatData 2026-07-03 12:07:46 +02:00
ARIA 9720a7befe Fix Equipment modal and json filter 2026-07-03 11:53:35 +02:00
ARIA 130998105a Added Agents.md 2026-07-03 11:24:11 +02:00
Pakobbix 411dc3eb9c Merge pull request 'feat: add Equipment system with slot validation and stat bonuses' (#1) from feature/equipment-system into main
Reviewed-on: #1
2026-07-03 09:16:57 +00:00
ARIA 10cfe581ac 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
2026-07-03 11:11:23 +02:00
Spicy_Marinara 38fb3d8c51 Fix tracker issues and add deprecation notice 2026-05-04 13:08:52 +02:00
Spicy Marinara 70792f8a2a Merge pull request #143 from SpicyMarinara/SpicyMarinara-patch-1
Update README to mark project as deprecated
2026-05-04 13:02:16 +02:00
22 changed files with 2207 additions and 75 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"MD013": false
}
+330
View File
@@ -0,0 +1,330 @@
# RPG Companion for SillyTavern — Agent Instructions
## What this is
A **browser extension for SillyTavern** (AI roleplay frontend). It is NOT a standalone Node.js application. All JavaScript runs in the browser context inside SillyTavern. There is **no build step** — files are loaded directly by SillyTavern's extension loader.
---
## Quick start
- **Entry point**: `index.js` (loaded by SillyTavern via `manifest.json`)
- **CSS**: `style.css` (single file, ~12,300 lines)
- **HTML templates**: `template.html` (main panel), `settings.html` (Extensions tab settings drawer)
- **Runtime**: Browser only. All code executes inside SillyTavern's page context.
- **jQuery**: Available globally as `$` / `jQuery`. Used extensively for DOM manipulation.
- **ES modules**: All `src/` files use `import`/`export` (browser ES module syntax).
---
## Manifest and loading
`manifest.json` tells SillyTavern how to load the extension:
```json
{
"display_name": "RPG Companion",
"loading_order": 100,
"js": "index.js",
"css": "style.css"
}
```
SillyTavern loads `index.js` and `style.css` relative to the extension directory. The extension name used internally is `third-party/rpg-companion-sillytavern`.
### Extension path detection
`src/core/config.js` auto-detects whether the extension is installed globally (`public/extensions/`) or per-user (`data/default-user/extensions/`) by inspecting `import.meta.url`. This determines template loading paths.
---
## SillyTavern integration
The extension imports from SillyTavern's global scripts using **relative paths** that assume the extension lives at:
- `scripts/extensions/third-party/rpg-companion-sillytavern/` (global install), or
- `data/default-user/extensions/third-party/rpg-companion-sillytavern/` (user install)
Key imports in `index.js`:
```js
import { getContext, renderExtensionTemplateAsync, extension_settings as st_extension_settings }
from '../../../extensions.js';
import { eventSource, event_types, substituteParams, chat, saveSettingsDebounced, ... }
from '../../../../script.js';
import { selected_group, getGroupMembers } from '../../../group-chats.js';
import { power_user } from '../../../power-user.js';
```
The deeper relative imports in `src/` files (e.g., `../../../../../../script.js`) follow the same pattern adjusted for directory depth.
### SillyTavern globals available at runtime
- `self.SillyTavern` — global namespace object
- `$` / `jQuery` — jQuery
- `toastr` — toast notification library
- `eventSource` — SillyTavern's event emitter
- `chat` — current chat message array
- `chat_metadata` — per-chat metadata store
- `characters` — loaded character array
- `getContext()` — returns current chat context
---
## Architecture
### Source tree (`src/`)
```
src/
├── core/ # Core infrastructure
│ ├── config.js # Extension name, folder path, default settings
│ ├── state.js # Centralized state variables + getters/setters
│ ├── persistence.js # Save/load settings, chat data, migrations
│ ├── events.js # SillyTavern event system wrapper
│ └── i18n.js # Translation system (fetches JSON, applies data-i18n-key)
├── i18n/ # Translation JSON files
│ ├── en.json # English (reference, 513 lines)
│ ├── fr.json # French
│ ├── ru.json # Russian
│ ├── zh-cn.json # Simplified Chinese
│ ├── zh-tw.json # Traditional Chinese
│ └── validator.js # Node.js script to validate translation consistency
├── systems/ # Feature systems (organized by concern)
│ ├── generation/ # AI prompt generation, parsing, API calls
│ │ ├── promptBuilder.js # Builds tracker prompts for AI
│ │ ├── parser.js # Parses AI responses to extract tracker data
│ │ ├── apiClient.js # Makes API calls for separate/external generation
│ │ ├── injector.js # Injects tracker prompts into generation context
│ │ ├── lockManager.js # Manages locked tracker fields
│ │ ├── suppression.js # Controls when to skip tracker injection
│ │ ├── encounterPrompts.js # Combat encounter prompt building
│ │ ├── inventoryParser.js # Parses inventory data from AI responses
│ │ └── jsonPromptHelpers.js # JSON formatting helpers for prompts
│ ├── rendering/ # UI rendering functions
│ │ ├── userStats.js # Renders user stats panel
│ │ ├── infoBox.js # Renders info box (date, weather, location, events)
│ │ ├── thoughts.js # Renders character thoughts + chat bubbles
│ │ ├── inventory.js # Renders inventory section
│ │ ├── equipment.js # Renders equipment section
│ │ ├── quests.js # Renders quests section
│ │ └── musicPlayer.js # Renders Spotify music player
│ ├── ui/ # UI components and helpers
│ │ ├── theme.js # Theme system (default, sci-fi, fantasy, cyberpunk, custom)
│ │ ├── modals.js # Settings modal, dice modal, deprecation modal
│ │ ├── layout.js # Panel positioning, collapse/expand, section visibility
│ │ ├── mobile.js # Mobile FAB button, mobile tabs, keyboard handling
│ │ ├── desktop.js # Desktop strip widgets
│ │ ├── trackerEditor.js # Tracker configuration editor
│ │ ├── promptsEditor.js # Custom prompt editor
│ │ ├── checkpointUI.js # Chapter checkpoint buttons
│ │ ├── encounterUI.js # Combat encounter modal
│ │ ├── snowflakes.js # Snowflake animation effect
│ │ ├── weatherEffects.js # Dynamic weather visual effects
│ │ └── alternatePresentCharacters.js # Below-chat character panel
│ ├── integration/ # SillyTavern event integration
│ │ ├── sillytavern.js # Event handlers: message sent/received, swipe, etc.
│ │ └── thoughtBasedExpressions.js # Integrates with ST Character Expressions
│ ├── interaction/ # User interaction handlers
│ │ ├── inventoryActions.js # Inventory click/edit handlers
│ │ ├── equipmentActions.js # Equipment click/edit handlers
│ │ └── inventoryEdit.js # Inventory inline editing
│ └── features/ # Feature modules
│ ├── plotProgression.js # Plot progression buttons
│ ├── classicStats.js # D&D-style attribute buttons
│ ├── dice.js # Dice roller
│ ├── htmlCleaning.js # Regex-based HTML tag cleaning
│ ├── jsonCleaning.js # Regex-based JSON cleanup in messages
│ ├── musicPlayer.js # Spotify URL parsing and embedding
│ ├── chapterCheckpoint.js # Save/restore tracker state checkpoints
│ ├── avatarGenerator.js # Auto-generate character avatars
│ └── encounterState.js # Combat encounter state management
├── types/ # JSDoc type definitions
│ └── inventory.js # InventoryV2 type definition
└── utils/ # Utility functions
├── avatars.js # Avatar URL handling
├── imageUrls.js # Image URL utilities
├── itemParser.js # Item string parsing
├── jsonMigration.js # v2 to v3 JSON format migration
├── jsonRepair.js # JSON repair for malformed AI responses
├── migration.js # Settings migration helpers
├── presentCharacters.js # Present character data helpers
├── responseExtractor.js # Extract tracker data from AI responses
├── security.js # Input validation and sanitization
├── sillyTavernExpressions.js # ST expression mapping
├── thoughtBasedExpressionPortraits.js # Expression-to-portrait mapping
└── transformations.js # Data transformation utilities
```
### Data flow
1. **User sends message**`MESSAGE_SENT` event fires
2. **Extension hooks** `onMessageSent` in `sillytavern.js`
3. **Together mode**: Tracker prompt is injected into the generation context via `injector.js`
4. **Separate mode**: After main response, `apiClient.js` makes a separate API call
5. **External API mode**: Uses user-configured external API endpoint
6. **AI responds**`MESSAGE_RECEIVED` event fires
7. **Extension hooks** `onMessageReceived``parser.js` extracts tracker data
8. **Rendering**: `renderUserStats()`, `renderInfoBox()`, `renderThoughts()`, etc. update the UI
9. **Persistence**: Tracker data is saved to `chat_metadata` per chat, and per-swipe
### State management
All extension state lives in `src/core/state.js` as module-level `let` variables with getter/setter functions. Key state:
- `extensionSettings` — persisted settings object (merged with defaults on load)
- `lastGeneratedData` — tracker data from the last AI generation
- `committedTrackerData` — tracker data committed after a message
- `lastActionWasSwipe` — flag to handle swipe-specific logic
- `isGenerating` — flag to prevent re-entrant generation
- `pendingDiceRoll` — dice roll result to inject into next generation
- DOM element caches: `$panelContainer`, `$userStatsContainer`, etc.
### Persistence
`src/core/persistence.js` handles:
- Extension settings: saved via SillyTavern's `saveSettingsDebounced()`
- Chat-specific data: saved in `chat_metadata.rpg_companion` per chat
- Per-swipe data: stored on each chat message object
- Settings versioning: automatic migration via `CURRENT_SETTINGS_VERSION` (currently 7)
- Deferred saves: batches chat data saves to avoid excessive writes
---
## Development commands
```bash
# Validate translation files (check missing keys, type mismatches, empty values)
npm run validate_locale_once
# Watch mode: re-validate on file changes
npm run validate_locale
```
No build, lint, test, or typecheck commands exist. The code runs directly in the browser.
---
## i18n system
### How it works
1. Translation files are flat JSON in `src/i18n/{locale}.json`
2. Keys use dot notation: `"template.settingsModal.display.showUserStats"`
3. HTML elements use `data-i18n-key`, `data-i18n-title`, or `data-i18n-aria-label` attributes
4. `src/core/i18n.js` fetches translations via `fetch()` from the extension path
5. `applyTranslations()` walks the DOM and updates `textContent`, `title`, and `aria-label`
### Adding translations
1. Add keys to `src/i18n/en.json` (reference locale)
2. Add corresponding keys to all other locale files
3. Add `data-i18n-key="your.key.here"` to HTML elements in `template.html` or `settings.html`
4. For dynamically generated text, use `i18n.getTranslation('your.key.here')` in JS
### Validator
`src/i18n/validator.js` is a **Node.js script** (uses `require()`, not ES modules). It:
- Compares all locales against the reference locale for missing/extra keys
- Checks for type mismatches
- Checks for empty strings and null values
- Scans `.html`/`.js`/`.jsx` files for unlocalized text (text in tags without `data-i18n-key`)
---
## Key conventions
### jQuery usage
DOM manipulation uses jQuery throughout. Element caches are stored as jQuery objects with `$` prefix:
```js
$panelContainer = $('#rpg-companion-panel')
$userStatsContainer = $('#rpg-user-stats')
```
### Event system
All SillyTavern event registration goes through `src/core/events.js`:
```js
registerAllEvents({
[event_types.MESSAGE_SENT]: onMessageSent,
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar],
});
```
### CSS naming
All CSS classes use `rpg-` prefix to avoid conflicts with SillyTavern and other extensions:
- `.rpg-panel`, `.rpg-section`, `.rpg-stat-bar`, `.rpg-btn-primary`, etc.
- CSS custom properties: `--rpg-bg`, `--rpg-accent`, `--rpg-text`, `--rpg-highlight`, `--rpg-border`, `--rpg-shadow`
### Panel structure
The main panel (`template.html`) has these sections in order:
1. Collapse toggle button
2. Strip widget container (shown when panel is collapsed)
3. Game container with header
4. Dice roll display
5. Content box with sections: User Stats → Info Box → Thoughts → Inventory → Equipment → Quests → Music Player
6. Feature toggles row (HTML, Dialogue Coloring, Deception, Omniscience, CYOA, Spotify, Weather, Narrator, Auto Avatars)
7. Manual update button
8. Settings and Edit Trackers buttons
### Generation modes
Three modes control how tracker data is generated:
- **`together`**: Tracker prompt injected into main generation. Single API call, faster, but tracker JSON mixed in response.
- **`separate`**: Two API calls — one for roleplay, one for tracker data. Cleaner roleplay, slower.
- **`external`**: Uses user-configured external API (OpenAI-compatible endpoint) for tracker generation.
### Guided generation compatibility
The `skipInjectionsForGuided` setting controls tracker injection behavior during guided generations:
- `'none'` — always inject (default)
- `'impersonation'` — skip only for impersonation-style guided generations
- `'guided'` — skip for any guided/instruct or quiet_prompt generation
---
## Important gotchas
1. **No bundler**: All imports use relative paths that depend on the extension's directory structure within SillyTavern. Changing file locations requires updating import paths.
2. **Relative import depth matters**: Files in `src/` use deeper relative paths (`../../../../../../script.js`) than `index.js` (`../../../../script.js`). When moving files between directories, adjust accordingly.
3. **Settings merge**: On load, saved settings are deep-merged with defaults from `src/core/state.js`. New settings fields should be added to both the state module (runtime defaults) AND `src/core/config.js` (documentation defaults).
4. **Chat metadata**: Per-chat tracker data lives in `chat_metadata.rpg_companion`. Per-swipe data lives on individual chat message objects in the `swipe_store`.
5. **i18n fetch path**: Translations are fetched at `/scripts/extensions/third-party/rpg-companion-sillytavern/src/i18n/{lang}.json`. The hardcoded path in `i18n.js` must match the extension's actual URL path.
6. **Extension enable/disable**: The extension can be toggled from SillyTavern's Extensions tab. When disabled, all UI elements are removed from the DOM. When re-enabled, `initUI()` rebuilds everything.
7. **Mobile vs desktop**: Viewport breakpoint is 1000px. Below: FAB button + mobile tabs. Above: fixed panel + desktop tabs.
8. **External API key**: Stored in `localStorage` as `rpg_companion_external_api_key`, NOT in extension settings (for security).
9. **The `package.json` has no runtime dependencies**. The `devDependencies` (chokidar, fs-extra, glob) are only for the i18n validator.
10. **Deprecation status**: The README states this extension is deprecated in favor of Marinara Engine. The code shows a deprecation modal that displays on first load.
---
## File reference
| File | Purpose |
|------|---------|
| `index.js` | Main entry point. All imports, UI init, event registration, startup logic. |
| `manifest.json` | SillyTavern extension manifest (name, loading order, JS/CSS files). |
| `style.css` | All CSS (~12,300 lines). Themes, animations, responsive breakpoints. |
| `template.html` | Main panel HTML template. Rendered by SillyTavern's `renderExtensionTemplateAsync`. |
| `settings.html` | Extensions tab settings drawer. Minimal — most settings are in the panel modal. |
| `src/core/state.js` | Central state module. All extension-wide variables live here. |
| `src/core/persistence.js` | Settings/chat data save/load, version migrations. |
| `src/core/config.js` | Extension name, path detection, default settings documentation. |
| `src/core/events.js` | SillyTavern event system wrapper with bulk register/unregister. |
| `src/core/i18n.js` | Translation loader and DOM applier. |
| `src/systems/integration/sillytavern.js` | All SillyTavern event handlers (message lifecycle, swipes, chat changes). |
| `src/systems/generation/promptBuilder.js` | Builds AI prompts for tracker generation. |
| `src/systems/generation/parser.js` | Parses AI responses to extract tracker JSON. |
| `src/systems/generation/apiClient.js` | API client for separate/external generation modes. |
| `src/systems/generation/injector.js` | Injects tracker prompts into SillyTavern's generation context. |
| `src/i18n/validator.js` | Node.js script for validating translation consistency. |
+11 -4
View File
@@ -11,7 +11,7 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
Moving on to developing the Marinara Engine frontend, the extension will now be maintained by the community! Moving on to developing the Marinara Engine frontend, the extension will now be maintained by the community!
https://github.com/Pasta-Devs/Marinara-Engine <https://github.com/Pasta-Devs/Marinara-Engine>
## 📥 Installation ## 📥 Installation
@@ -21,7 +21,7 @@ https://github.com/Pasta-Devs/Marinara-Engine
3. Go to Install extension 3. Go to Install extension
4. Copy-paste this link: https://github.com/SpicyMarinara/rpg-companion-sillytavern 4. Copy-paste this link: <https://github.com/SpicyMarinara/rpg-companion-sillytavern>
5. Press Install for all users/Install just for me 5. Press Install for all users/Install just for me
@@ -99,11 +99,13 @@ AI: Trackers + Full roleplay response
↓ Main chat shows clean roleplay text ↓ Main chat shows clean roleplay text
Pros: Pros:
- Single API call - Single API call
- Faster response - Faster response
- Simpler setup - Simpler setup
Cons: Cons:
- Tracker formatting mixed in AI response - Tracker formatting mixed in AI response
- May affect roleplay quality slightly - May affect roleplay quality slightly
@@ -127,11 +129,13 @@ AI: Separate call with just the tracker data
↓ Context summary injected into the next generation ↓ Context summary injected into the next generation
Pros: Pros:
- Clean roleplay responses - Clean roleplay responses
- Better roleplay quality - Better roleplay quality
- Contextual summary enhances immersion - Contextual summary enhances immersion
Cons: Cons:
- Extra API call - Extra API call
- Slightly slower - Slightly slower
@@ -163,16 +167,19 @@ You can edit most fields by clicking on them:
Access comprehensive customization through the Tracker Settings button: Access comprehensive customization through the Tracker Settings button:
**User Stats Configuration:** **User Stats Configuration:**
- Add/remove custom stats with unique names - Add/remove custom stats with unique names
- Configure Status section (mood emoji + custom fields) - Configure Status section (mood emoji + custom fields)
- Configure Skills section with custom skill fields - Configure Skills section with custom skill fields
- Toggle RPG attributes display - Toggle RPG attributes display
**Info Box Configuration:** **Info Box Configuration:**
- Enable/disable individual widgets (Date, Weather, Temperature, Time, Location, Recent Events) - Enable/disable individual widgets (Date, Weather, Temperature, Time, Location, Recent Events)
- Choose temperature unit (Celsius/Fahrenheit) - Choose temperature unit (Celsius/Fahrenheit)
**Present Characters Configuration:** **Present Characters Configuration:**
- Add custom character fields (appearance, action, demeanor, etc.) - Add custom character fields (appearance, action, demeanor, etc.)
- Configure relationship status options - Configure relationship status options
- Enable character-specific stats tracking - Enable character-specific stats tracking
@@ -199,11 +206,11 @@ This extension detects when a "guided generation" prompt is submitted (for examp
If you want tracker prompts to apply during a guided generation, run the update via separate generation or temporarily disable guided generation in the other extension. If you want tracker prompts to apply during a guided generation, run the update via separate generation or temporarily disable guided generation in the other extension.
There is a new setting "Skip Tracker & HTML Injections during Guided Generations" in the RPG Companion settings (Advanced section). It now supports three modes: There is a new setting "Skip Tracker & HTML Injections during Guided Generations" in the RPG Companion settings (Advanced section). It now supports three modes:
- none: never skip (always inject the tracker prompts as usual, default) - none: never skip (always inject the tracker prompts as usual, default)
- impersonation: only skip when an impersonation-style guided generation is detected - impersonation: only skip when an impersonation-style guided generation is detected
- guided: skip whenever a guided `instruct` or `quiet_prompt` generation is detected - guided: skip whenever a guided `instruct` or `quiet_prompt` generation is detected
## 🎨 Themes ## 🎨 Themes
Choose from 6 beautiful themes: Choose from 6 beautiful themes:
@@ -286,4 +293,4 @@ SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDea
Made with ❤️ by Marinara Made with ❤️ by Marinara
PS I'm looking for a job or a sponsor to fund my custom AI frontend, contact me if interested: PS I'm looking for a job or a sponsor to fund my custom AI frontend, contact me if interested:
mgrabower97@gmail.com [mgrabower97@gmail.com](mailto:mgrabower97@gmail.com)
+25 -6
View File
@@ -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 {
@@ -95,7 +99,8 @@ import {
updateDiceDisplay, updateDiceDisplay,
addDiceQuickReply, addDiceQuickReply,
getSettingsModal, getSettingsModal,
showWelcomeModalIfNeeded showWelcomeModalIfNeeded,
showDeprecationModalIfNeeded
} from './src/systems/ui/modals.js'; } from './src/systems/ui/modals.js';
import { import {
initTrackerEditor initTrackerEditor
@@ -312,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'));
@@ -388,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();
@@ -402,6 +414,7 @@ async function initUI() {
renderInfoBox(); renderInfoBox();
renderThoughts(); renderThoughts();
renderInventory(); renderInventory();
renderEquipment();
renderQuests(); renderQuests();
}); });
@@ -861,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
@@ -880,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() {
@@ -1120,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);
@@ -1270,6 +1284,7 @@ async function initUI() {
renderInfoBox(); renderInfoBox();
renderThoughts(); renderThoughts();
renderInventory(); renderInventory();
renderEquipment();
renderQuests(); renderQuests();
renderMusicPlayer($musicPlayerContainer[0]); renderMusicPlayer($musicPlayerContainer[0]);
updateDiceDisplay(); updateDiceDisplay();
@@ -1283,6 +1298,7 @@ async function initUI() {
setupMobileKeyboardHandling(); setupMobileKeyboardHandling();
setupContentEditableScrolling(); setupContentEditableScrolling();
initInventoryEventListeners(); initInventoryEventListeners();
initEquipmentEventListeners();
// Initialize chapter checkpoint UI // Initialize chapter checkpoint UI
initChapterCheckpointUI(); initChapterCheckpointUI();
@@ -1511,11 +1527,14 @@ jQuery(async () => {
// Non-critical - continue without it // Non-critical - continue without it
} }
// Show welcome modal for v3.0 on first launch // Show deprecation notice once for this release; otherwise keep the old welcome flow.
try { try {
showWelcomeModalIfNeeded(); const deprecationModalShown = showDeprecationModalIfNeeded();
if (!deprecationModalShown) {
showWelcomeModalIfNeeded();
}
} catch (error) { } catch (error) {
console.error('[RPG Companion] Welcome modal failed:', error); console.error('[RPG Companion] Startup modal failed:', error);
// Non-critical - continue without it // Non-critical - continue without it
} }
+1 -1
View File
@@ -6,6 +6,6 @@
"js": "index.js", "js": "index.js",
"css": "style.css", "css": "style.css",
"author": "Marinara", "author": "Marinara",
"version": "3.7.3", "version": "3.7.4",
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern" "homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "rpg-complanion-sillytavern", "name": "rpg-complanion-sillytavern",
"version": "3.7.3", "version": "3.7.4",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
+369 -28
View File
@@ -3,7 +3,7 @@
* Handles saving/loading extension settings and chat data * Handles saving/loading extension settings and chat data
*/ */
import { saveSettingsDebounced, chat_metadata, saveChatDebounced } from '../../../../../../script.js'; import { saveSettingsDebounced, chat_metadata, saveChatDebounced, getCurrentChatId } from '../../../../../../script.js';
import { getContext } from '../../../../../extensions.js'; import { getContext } from '../../../../../extensions.js';
import { import {
extensionSettings, extensionSettings,
@@ -23,6 +23,269 @@ 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 = 7;
const DEFAULT_USER_STATS = {
health: 100,
satiety: 100,
energy: 100,
hygiene: 100,
arousal: 0,
mood: '😐',
conditions: 'None',
skills: [],
inventory: {
version: 2,
onPerson: "None",
clothing: "None",
stored: {},
assets: "None"
},
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
}
}
};
const DEFAULT_EXTENSION_SETTINGS = cloneSerializable(extensionSettings);
DEFAULT_EXTENSION_SETTINGS.settingsVersion = CURRENT_SETTINGS_VERSION;
let hasDeferredChatDataSave = false;
function cloneSerializable(value) {
if (value === undefined) {
return undefined;
}
try {
return structuredClone(value);
} catch {
return JSON.parse(JSON.stringify(value));
}
}
function isPlainObject(value) {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function mergeWithDefaults(defaultValue, savedValue) {
if (savedValue === undefined) {
return cloneSerializable(defaultValue);
}
if (isPlainObject(defaultValue) && isPlainObject(savedValue)) {
const merged = cloneSerializable(defaultValue);
for (const [key, value] of Object.entries(savedValue)) {
merged[key] = mergeWithDefaults(defaultValue[key], value);
}
return merged;
}
return cloneSerializable(savedValue);
}
function parseMaybeJSON(value) {
if (typeof value !== 'string') {
return value;
}
const trimmed = value.trim();
if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) {
return value;
}
try {
return JSON.parse(trimmed);
} catch {
return value;
}
}
function stringifyInventoryItems(items) {
if (typeof items === 'string') {
return items.trim() || 'None';
}
if (!Array.isArray(items)) {
return 'None';
}
const text = items
.map(item => {
if (isPlainObject(item) && item.name) {
const quantity = Number(item.quantity);
return quantity > 1 ? `${quantity}x ${item.name}` : item.name;
}
return String(item || '').trim();
})
.filter(Boolean)
.join(', ');
return text || 'None';
}
function normalizeStoredInventory(stored) {
if (!isPlainObject(stored)) {
return {};
}
const normalized = {};
for (const [location, items] of Object.entries(stored)) {
normalized[location] = stringifyInventoryItems(items);
}
return normalized;
}
function normalizeInventoryValue(inventory) {
const parsedInventory = parseMaybeJSON(inventory);
if (isPlainObject(parsedInventory) && (
Array.isArray(parsedInventory.onPerson)
|| Array.isArray(parsedInventory.clothing)
|| Array.isArray(parsedInventory.assets)
|| isPlainObject(parsedInventory.stored)
)) {
return {
version: 2,
onPerson: stringifyInventoryItems(parsedInventory.onPerson),
clothing: stringifyInventoryItems(parsedInventory.clothing),
stored: normalizeStoredInventory(parsedInventory.stored),
assets: stringifyInventoryItems(parsedInventory.assets)
};
}
const migrationResult = migrateInventory(parsedInventory);
return mergeWithDefaults(DEFAULT_USER_STATS.inventory, migrationResult.inventory);
}
function normalizeUserStatsValue(userStats) {
const parsedStats = parseMaybeJSON(userStats);
const normalized = cloneSerializable(DEFAULT_USER_STATS);
if (!isPlainObject(parsedStats)) {
return normalized;
}
if (Array.isArray(parsedStats.stats)) {
for (const stat of parsedStats.stats) {
if (!stat || typeof stat !== 'object') continue;
const id = stat.id || stat.name?.toLowerCase?.();
if (id && stat.value !== undefined) {
normalized[id] = stat.value;
}
}
} else {
for (const [key, value] of Object.entries(parsedStats)) {
if (!['stats', 'status', 'inventory', 'quests'].includes(key) && value !== undefined) {
normalized[key] = cloneSerializable(value);
}
}
}
if (isPlainObject(parsedStats.status)) {
for (const [key, value] of Object.entries(parsedStats.status)) {
if (value !== undefined) {
normalized[key] = cloneSerializable(value);
}
}
}
if (parsedStats.inventory !== undefined) {
normalized.inventory = normalizeInventoryValue(parsedStats.inventory);
}
for (const [key, defaultValue] of Object.entries(DEFAULT_USER_STATS)) {
if (typeof defaultValue !== 'number') continue;
const numericValue = Number(normalized[key]);
normalized[key] = Number.isFinite(numericValue) ? numericValue : defaultValue;
}
return mergeWithDefaults(DEFAULT_USER_STATS, normalized);
}
function normalizeQuestValue(quest) {
let value = quest;
while (isPlainObject(value) && value.value !== undefined) {
value = value.value;
}
if (typeof value === 'string') {
return value.trim() || 'None';
}
if (isPlainObject(value)) {
return value.title || value.description || JSON.stringify(value);
}
return value == null ? 'None' : String(value);
}
function normalizeQuestsValue(quests) {
if (!isPlainObject(quests)) {
return { main: 'None', optional: [] };
}
const optionalSource = Array.isArray(quests.optional)
? quests.optional
: (Array.isArray(quests.active) ? quests.active : []);
return {
main: normalizeQuestValue(quests.main),
optional: optionalSource
.map(normalizeQuestValue)
.filter(quest => quest && quest !== 'None')
};
}
function normalizeSettings(savedSettings) {
const sourceSettings = isPlainObject(savedSettings) ? savedSettings : {};
const normalized = mergeWithDefaults(DEFAULT_EXTENSION_SETTINGS, sourceSettings);
const savedVersion = Number(sourceSettings.settingsVersion);
normalized.settingsVersion = Number.isFinite(savedVersion) && savedVersion > 0 ? savedVersion : 1;
normalized.userStats = normalizeUserStatsValue(sourceSettings.userStats);
const parsedUserStats = parseMaybeJSON(sourceSettings.userStats);
if (sourceSettings.quests !== undefined) {
normalized.quests = normalizeQuestsValue(sourceSettings.quests);
} else if (isPlainObject(parsedUserStats) && parsedUserStats.quests !== undefined) {
normalized.quests = normalizeQuestsValue(parsedUserStats.quests);
}
return {
settings: normalized,
changed: JSON.stringify(normalized) !== JSON.stringify(savedSettings)
};
}
function isChatDataSaveReady() {
return !!(
chat_metadata
&& typeof chat_metadata === 'object'
&& chat_metadata.integrity
&& getCurrentChatId()
);
}
function hasTrackerPayload(payload) { function hasTrackerPayload(payload) {
return !!(payload && typeof payload === 'object' && ( return !!(payload && typeof payload === 'object' && (
@@ -273,7 +536,8 @@ function validateSettings(settings) {
// Check for required top-level properties // Check for required top-level properties
if (typeof settings.enabled !== 'boolean' || if (typeof settings.enabled !== 'boolean' ||
typeof settings.autoUpdate !== 'boolean' || typeof settings.autoUpdate !== 'boolean' ||
!settings.userStats || typeof settings.userStats !== 'object') { !settings.userStats || typeof settings.userStats !== 'object' ||
Array.isArray(settings.userStats)) {
console.warn('[RPG Companion] Settings validation failed: missing required properties'); console.warn('[RPG Companion] Settings validation failed: missing required properties');
return false; return false;
} }
@@ -282,7 +546,8 @@ function validateSettings(settings) {
const stats = settings.userStats; const stats = settings.userStats;
if (typeof stats.health !== 'number' || if (typeof stats.health !== 'number' ||
typeof stats.satiety !== 'number' || typeof stats.satiety !== 'number' ||
typeof stats.energy !== 'number') { typeof stats.energy !== 'number' ||
!stats.inventory || typeof stats.inventory !== 'object') {
console.warn('[RPG Companion] Settings validation failed: invalid userStats structure'); console.warn('[RPG Companion] Settings validation failed: invalid userStats structure');
return false; return false;
} }
@@ -307,21 +572,23 @@ export function loadSettings() {
if (extension_settings[extensionName]) { if (extension_settings[extensionName]) {
const savedSettings = extension_settings[extensionName]; const savedSettings = extension_settings[extensionName];
const normalizedResult = normalizeSettings(savedSettings);
const normalizedSettings = normalizedResult.settings;
// Validate loaded settings // Validate loaded settings after schema repair/normalization
if (!validateSettings(savedSettings)) { if (!validateSettings(normalizedSettings)) {
console.warn('[RPG Companion] Loaded settings failed validation, using defaults'); console.warn('[RPG Companion] Loaded settings failed validation, using defaults');
console.warn('[RPG Companion] Invalid settings:', savedSettings); console.warn('[RPG Companion] Invalid settings:', normalizedSettings);
// Save valid defaults to replace corrupt data // Save valid defaults to replace corrupt data
saveSettings(); saveSettings();
return; return;
} }
updateExtensionSettings(savedSettings); updateExtensionSettings(normalizedSettings);
// Perform settings migrations based on version // Perform settings migrations based on version
const currentVersion = extensionSettings.settingsVersion || 1; const currentVersion = extensionSettings.settingsVersion || 1;
let settingsChanged = false; let settingsChanged = normalizedResult.changed;
// Migration to version 2: Enable dynamic weather for existing users // Migration to version 2: Enable dynamic weather for existing users
if (currentVersion < 2) { if (currentVersion < 2) {
@@ -373,6 +640,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';
@@ -455,7 +785,8 @@ export function saveSettings() {
* Saves RPG data to the current chat's metadata. * Saves RPG data to the current chat's metadata.
*/ */
export function saveChatData() { export function saveChatData() {
if (!chat_metadata) { if (!isChatDataSaveReady()) {
hasDeferredChatDataSave = true;
return; return;
} }
@@ -480,6 +811,16 @@ export function saveChatData() {
saveChatDebounced(); saveChatDebounced();
} }
export function flushDeferredChatDataSave() {
if (!hasDeferredChatDataSave || !isChatDataSaveReady()) {
return false;
}
hasDeferredChatDataSave = false;
saveChatData();
return true;
}
/** /**
* Mirrors a tracker data entry into message.swipe_info so it survives page reloads. * Mirrors a tracker data entry into message.swipe_info so it survives page reloads.
* ST only serializes swipe_info to disk; message.extra is in-memory only. * ST only serializes swipe_info to disk; message.extra is in-memory only.
@@ -699,22 +1040,7 @@ export function loadChatData() {
if (!savedData) { if (!savedData) {
// Reset to defaults if no metadata exists, then try to rebuild from message swipe data below. // Reset to defaults if no metadata exists, then try to rebuild from message swipe data below.
updateExtensionSettings({ updateExtensionSettings({
userStats: { userStats: cloneSerializable(DEFAULT_USER_STATS),
health: 100,
satiety: 100,
energy: 100,
hygiene: 100,
arousal: 0,
mood: '😐',
conditions: 'None',
// Use v2 inventory format for defaults
inventory: {
version: 2,
onPerson: "None",
stored: {},
assets: "None"
}
},
quests: { quests: {
main: "None", main: "None",
optional: [] optional: []
@@ -734,9 +1060,10 @@ export function loadChatData() {
clearThoughtBasedExpressionPortraits(); clearThoughtBasedExpressionPortraits();
} }
// Restore stats // Restore stats — merge with defaults to preserve properties like `equipment`
// that may not exist in older saves
if (savedData?.userStats) { if (savedData?.userStats) {
extensionSettings.userStats = { ...savedData.userStats }; extensionSettings.userStats = mergeWithDefaults(DEFAULT_USER_STATS, savedData.userStats);
} }
// Restore classic stats // Restore classic stats
@@ -830,6 +1157,7 @@ function validateInventoryStructure(inventory, source) {
extensionSettings.userStats.inventory = { extensionSettings.userStats.inventory = {
version: 2, version: 2,
onPerson: "None", onPerson: "None",
clothing: "None",
stored: {}, stored: {},
assets: "None" assets: "None"
}; };
@@ -861,6 +1189,20 @@ function validateInventoryStructure(inventory, source) {
} }
} }
// Validate clothing field
if (typeof inventory.clothing !== 'string') {
console.warn(`[RPG Companion] Invalid clothing from ${source}, resetting to "None"`);
inventory.clothing = "None";
needsSave = true;
} else {
const cleanedClothing = cleanItemString(inventory.clothing);
if (cleanedClothing !== inventory.clothing) {
console.warn(`[RPG Companion] Cleaned corrupted items from clothing inventory (${source})`);
inventory.clothing = cleanedClothing;
needsSave = true;
}
}
// Validate stored field (CRITICAL for Bug #3) // Validate stored field (CRITICAL for Bug #3)
if (!inventory.stored || typeof inventory.stored !== 'object' || Array.isArray(inventory.stored)) { if (!inventory.stored || typeof inventory.stored !== 'object' || Array.isArray(inventory.stored)) {
console.error(`[RPG Companion] Corrupted stored inventory from ${source}, resetting to empty object`); console.error(`[RPG Companion] Corrupted stored inventory from ${source}, resetting to empty object`);
@@ -1559,4 +1901,3 @@ export function importPresets(importData, overwrite = false) {
return importCount; return importCount;
} }
+45 -19
View File
@@ -10,7 +10,7 @@
* Extension settings - persisted to SillyTavern settings * Extension settings - persisted to SillyTavern settings
*/ */
export let extensionSettings = { export let extensionSettings = {
settingsVersion: 4, // 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'
@@ -108,27 +109,47 @@ export let extensionSettings = {
stats: { enabled: true }, // All stats as compact numbers stats: { enabled: true }, // All stats as compact numbers
attributes: { enabled: true } // Compact RPG attributes display attributes: { enabled: true } // Compact RPG attributes display
}, },
userStats: JSON.stringify({ userStats: {
stats: [ health: 100,
{ id: 'health', name: 'Health', value: 100 }, satiety: 100,
{ id: 'satiety', name: 'Satiety', value: 100 }, energy: 100,
{ id: 'energy', name: 'Energy', value: 100 }, hygiene: 100,
{ id: 'hygiene', name: 'Hygiene', value: 100 }, arousal: 0,
{ id: 'arousal', name: 'Arousal', value: 0 } mood: '😐',
], conditions: 'None',
status: { skills: [],
mood: '😐',
conditions: 'None'
},
inventory: { inventory: {
onPerson: [], version: 2,
stored: [] onPerson: "None",
clothing: "None",
stored: {},
assets: "None"
}, },
quests: { equipment: {
active: [], items: [], // Array of {id, name, type, slot, stats: {str: 2, dex: 1, ...}, description}
completed: [] 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
}
} }
}, null, 2), },
statNames: { statNames: {
health: 'Health', health: 'Health',
satiety: 'Satiety', satiety: 'Satiety',
@@ -476,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
@@ -568,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;
}
+46
View File
@@ -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",
+41
View File
@@ -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;
}
+2
View File
@@ -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]);
+10 -2
View File
@@ -33,7 +33,8 @@ import {
setMessageSwipeTrackerData, setMessageSwipeTrackerData,
getSwipeData, getSwipeData,
commitTrackerDataFromPriorMessage, commitTrackerDataFromPriorMessage,
inheritSwipeDataFromPriorMessage inheritSwipeDataFromPriorMessage,
flushDeferredChatDataSave
} from '../../core/persistence.js'; } from '../../core/persistence.js';
import { i18n } from '../../core/i18n.js'; import { i18n } from '../../core/i18n.js';
@@ -49,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';
@@ -326,6 +328,7 @@ function rerenderRpgState() {
renderInfoBox(); renderInfoBox();
renderThoughts(); renderThoughts();
renderInventory(); renderInventory();
renderEquipment();
renderQuests(); renderQuests();
renderMusicPlayer($musicPlayerContainer[0]); renderMusicPlayer($musicPlayerContainer[0]);
updateFabWidgets(); updateFabWidgets();
@@ -389,6 +392,7 @@ export function onChatLoaded() {
restoreOrRepairLatestTrackerState(); restoreOrRepairLatestTrackerState();
maybeRehydrateUserStatsFromDisplayData(); maybeRehydrateUserStatsFromDisplayData();
rerenderRpgState(); rerenderRpgState();
flushDeferredChatDataSave();
scheduleChatStateRehydration(); scheduleChatStateRehydration();
updateAllCheckpointIndicators(); updateAllCheckpointIndicators();
} }
@@ -521,7 +525,8 @@ export async function onMessageReceived(data) {
// Remove the tracker code blocks from the visible message // Remove the tracker code blocks from the visible message
let cleanedMessage = responseText; let cleanedMessage = responseText;
// Note: JSON code blocks are hidden from display by regex script (but preserved in message data) // Remove JSON code blocks (v3 format) — primary defense, works regardless of regex script
cleanedMessage = cleanedMessage.replace(/```(?:json|markdown)?\s*[\s\S]*?```/gim, '');
// Remove old text format code blocks (legacy support) // Remove old text format code blocks (legacy support)
cleanedMessage = cleanedMessage.replace(/```[^`]*?Stats\s*\n\s*---[^`]*?```\s*/gi, ''); cleanedMessage = cleanedMessage.replace(/```[^`]*?Stats\s*\n\s*---[^`]*?```\s*/gi, '');
@@ -547,6 +552,7 @@ export async function onMessageReceived(data) {
renderInfoBox(); renderInfoBox();
renderThoughts(); renderThoughts();
renderInventory(); renderInventory();
renderEquipment();
renderQuests(); renderQuests();
renderMusicPlayer($musicPlayerContainer[0]); renderMusicPlayer($musicPlayerContainer[0]);
@@ -658,6 +664,7 @@ export function onCharacterChanged() {
// Load chat-specific data when switching chats // Load chat-specific data when switching chats
loadChatData(); loadChatData();
flushDeferredChatDataSave();
// chat_metadata may not reflect the actual chat tail for branches, so // chat_metadata may not reflect the actual chat tail for branches, so
// loadChatData() may have just restored stale data from the parent chat. // loadChatData() may have just restored stale data from the parent chat.
@@ -767,6 +774,7 @@ export function onMessageSwiped(messageIndex) {
renderInfoBox(); renderInfoBox();
renderThoughts(); renderThoughts();
renderInventory(); renderInventory();
renderEquipment();
renderQuests(); renderQuests();
renderMusicPlayer($musicPlayerContainer[0]); renderMusicPlayer($musicPlayerContainer[0]);
+578
View File
@@ -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 || { items: [], slots: {} };
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 || { items: [], slots: {} };
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 || { items: [], slots: {} };
// 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 || { items: [], slots: {} };
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 || { items: [], slots: {} };
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 || { items: [], slots: {} };
// 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 || { items: [], slots: {} };
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();
}
});
}
+157
View File
@@ -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 || { items: [], slots: {} };
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]);
}
+1 -1
View File
@@ -219,7 +219,7 @@ export function renderOptionalQuestsView(optionalQuests) {
* Main render function for quests * Main render function for quests
*/ */
export function renderQuests() { export function renderQuests() {
if (!extensionSettings.showInventory || !$questsContainer) { if (!extensionSettings.showQuests || !$questsContainer) {
return; return;
} }
+6 -2
View File
@@ -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,20 +411,23 @@ 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">
<div class="rpg-classic-stats-grid"> <div class="rpg-classic-stats-grid">
`; `;
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>
+28 -3
View File
@@ -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();
} }
+19 -5
View File
@@ -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) {
+30 -3
View File
@@ -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();
} }
+65
View File
@@ -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');
@@ -612,6 +614,28 @@ export function showWelcomeModalIfNeeded() {
} }
} }
/**
* Shows the deprecation notice once for users updating to the deprecation release.
* @returns {boolean} True when the modal was displayed.
*/
export function showDeprecationModalIfNeeded() {
const DEPRECATION_NOTICE_VERSION = '3.7.4';
const STORAGE_KEY = 'rpg_companion_deprecation_notice_seen';
try {
const seenVersion = localStorage.getItem(STORAGE_KEY);
if (seenVersion !== DEPRECATION_NOTICE_VERSION) {
showDeprecationModal(DEPRECATION_NOTICE_VERSION, STORAGE_KEY);
return true;
}
} catch (error) {
console.error('[RPG Companion] Failed to check deprecation modal status:', error);
}
return false;
}
/** /**
* Shows the welcome modal * Shows the welcome modal
* @param {string} version - The version to mark as seen * @param {string} version - The version to mark as seen
@@ -663,3 +687,44 @@ function showWelcomeModal(version, storageKey) {
} }
}, { once: true }); }, { once: true });
} }
function showDeprecationModal(version, storageKey) {
const modal = document.getElementById('rpg-deprecation-modal');
if (!modal) {
console.error('[RPG Companion] Deprecation modal element not found');
return;
}
const theme = extensionSettings.theme || 'default';
modal.setAttribute('data-theme', theme);
modal.style.display = 'flex';
modal.classList.add('is-open');
const closeBtn = document.getElementById('rpg-deprecation-close');
const gotItBtn = document.getElementById('rpg-deprecation-got-it');
const closeModal = () => {
modal.classList.add('is-closing');
setTimeout(() => {
modal.style.display = 'none';
modal.classList.remove('is-open', 'is-closing');
}, 200);
try {
localStorage.setItem(storageKey, version);
} catch (error) {
console.error('[RPG Companion] Failed to save deprecation modal status:', error);
}
};
closeBtn?.addEventListener('click', closeModal, { once: true });
gotItBtn?.addEventListener('click', closeModal, { once: true });
modal.addEventListener('click', (e) => {
if (e.target === modal) {
closeModal();
}
}, { once: true });
}
+386
View File
@@ -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, #1e1e2e);
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;
}
+53
View File
@@ -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 -->
@@ -212,6 +220,42 @@
</div> </div>
</div> </div>
<!-- Deprecation Notice Modal -->
<div id="rpg-deprecation-modal" class="rpg-settings-popup" role="dialog" aria-modal="true"
aria-labelledby="rpg-deprecation-title" style="display: none;">
<div class="rpg-settings-popup-content" style="max-width: 640px;">
<header class="rpg-settings-popup-header">
<h3 id="rpg-deprecation-title">
<i class="fa-solid fa-circle-info"></i>
<span data-i18n-key="deprecation.title">RPG Companion becomes deprecated!</span>
</h3>
<button id="rpg-deprecation-close" class="rpg-popup-close" type="button"
aria-label="Close Deprecation Notice">
<i class="fa-solid fa-times"></i>
</button>
</header>
<div class="rpg-settings-popup-body" style="max-height: 520px; overflow-y: auto; padding: 20px;">
<p data-i18n-key="deprecation.body.support">Thank you all for the continuous support. The extension will continue to function in its current state and will receive occasional bug fixes/features if provided by the community members. However, I (Marinara) won't be actively developing it further.</p>
<p>Why? The reason is simple, <strong data-i18n-key="deprecation.body.reasonEmphasis">I am no longer using SillyTavern as a frontend, and have instead moved on to develop my own frontend called MarinaraEngine.</strong> It's free, open-source, and plug-and-play, centered around utilizing agents, and already has all the RPG Companion's features built in and comes with a multitude of other, custom features (such as different chat modes, for Discord-styled conversations, classic Roleplay, and a brand new game mode that offers you an RPG with VN-style visuals).</p>
<p data-i18n-key="deprecation.linkIntro">If you're interested, check it out here:</p>
<p>
<a href="https://github.com/Pasta-Devs/Marinara-Engine" target="_blank" rel="noopener noreferrer" class="menu_button" style="display: inline-block;">
<i class="fa-brands fa-github"></i> MarinaraEngine
</a>
</p>
<p style="margin-top: 24px;"><strong><em data-i18n-key="deprecation.signoff">Cheers and happy gooning!</em></strong></p>
</div>
<footer class="rpg-settings-popup-footer">
<button id="rpg-deprecation-got-it" class="rpg-btn-primary" type="button" style="width: 100%;">
<i class="fa-solid fa-check"></i> <span data-i18n-key="global.gotIt">Got it!</span>
</button>
</footer>
</div>
</div>
<!-- Settings Modal --> <!-- Settings Modal -->
<div id="rpg-settings-popup" class="rpg-settings-popup" role="dialog" aria-modal="true" <div id="rpg-settings-popup" class="rpg-settings-popup" role="dialog" aria-modal="true"
aria-labelledby="rpg-settings-title"> aria-labelledby="rpg-settings-title">
@@ -412,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>