Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35cffa2696 | |||
| 10cfe581ac | |||
| 38fb3d8c51 | |||
| 70792f8a2a | |||
| 4cd0b72472 | |||
| 2d63e51962 | |||
| f142d04b48 | |||
| 45c074499c | |||
| ad66410f81 | |||
| ed9c12e969 | |||
| 60c430919b | |||
| 2ee081a619 | |||
| 19ebf1a834 | |||
| bda43208a2 | |||
| 781b28e02b | |||
| e018cddb15 | |||
| 05655fecf7 | |||
| 4e36667890 | |||
| c82eb03288 | |||
| 96d589adc0 | |||
| 55aa2a1e6a | |||
| 215a122797 | |||
| 1a11699522 | |||
| c79c941871 | |||
| 9ef5b16663 | |||
| 1d297aeecf | |||
| 4d2afafbaf | |||
| 08097e8b41 | |||
| e81afa4d74 | |||
| 0499cc6961 | |||
| 51b5360a7c | |||
| c73c0c2bb6 | |||
| 2f98686e60 | |||
| ae9e44eafb | |||
| 933d78e192 | |||
| 03345b81f4 | |||
| c307f1a1bc | |||
| c442314c10 | |||
| ce48ac0c34 | |||
| 8ea9044492 | |||
| 9213d264a0 | |||
| 32280d60ef | |||
| 75c8f9b63a | |||
| b1098a2721 | |||
| 131b28fc1f | |||
| 7305af8f88 | |||
| 66a22c74d0 | |||
| 5b2cb331c8 | |||
| bb202aca9c | |||
| 733647084a | |||
| ab848828e7 | |||
| 4d0de8419c | |||
| 76beb5dff4 | |||
| 178ced00be | |||
| 979525e372 | |||
| d96a199890 | |||
| 4b816dd1fd | |||
| 8f2dbd2f88 | |||
| f3e7518622 | |||
| 502646bb92 | |||
| 98fd702175 | |||
| 25aedce786 | |||
| 105e20e97a | |||
| 5498c64f5d | |||
| 5fa369e3d7 | |||
| 52be8dca1f | |||
| 32c4f67822 | |||
| b61a426efe | |||
| 2a77c091dd | |||
| c0431a6117 | |||
| 43610bf8b6 | |||
| 2a5b57087b | |||
| 653d23ef9a | |||
| ea81dd0634 | |||
| 7a3487c741 | |||
| 6fc35e50a1 | |||
| e82918004e | |||
| f78c8a1b78 | |||
| 2a48c30808 | |||
| c5a9c8631f | |||
| 2623df4050 | |||
| 03f21ef1ef | |||
| 2e747bc8aa | |||
| d0dd8950a6 | |||
| 5ddc380dac | |||
| f4324a5d19 | |||
| 4612ed2108 | |||
| 0e988b201c | |||
| 7b4ebb8d76 | |||
| 08474bd910 | |||
| 0bb2085305 | |||
| c6f13d18ff | |||
| 334f5fa5a3 | |||
| 5f9d67ebe8 | |||
| 93c37c25d7 | |||
| 0499f2c43e | |||
| 35bd55615b | |||
| f38f6850c3 | |||
| 989f511d01 | |||
| b827b77184 | |||
| 4f3d59bfb7 | |||
| c18fd39283 | |||
| f5825a7a24 | |||
| c14250e467 | |||
| e8edc42164 | |||
| acf119d4b4 | |||
| 6582095cc1 | |||
| 8aaf258ba3 | |||
| 7c1c140a2a | |||
| ce668c4793 | |||
| 3d6db2b0e9 | |||
| 2151b2dae3 | |||
| 4644e0fd93 | |||
| b18aaee0c0 | |||
| 0066b61746 | |||
| 6e9ff9812d | |||
| 3797e21912 | |||
| 7bac0d48f9 | |||
| 7081137fe3 | |||
| 3ceb64c3bd | |||
| 831c230b36 | |||
| 3a6acb37be | |||
| ce8db67de4 | |||
| 0262218ad0 | |||
| 3fc2cfa8ab | |||
| c614f7b8dc | |||
| 46e6de0eba | |||
| e2a48a4075 | |||
| 8d41010509 | |||
| 95d5616141 | |||
| 5918e38ade | |||
| bb3028adbb | |||
| bc4f50a82f | |||
| 126cfedaa4 | |||
| f3deead868 | |||
| d5d649f122 | |||
| 0cd764c39b | |||
| b9a15722d6 | |||
| 995f3a7a98 | |||
| db97f012b0 | |||
| 681b8ba2bc | |||
| 2d961936c2 | |||
| b534bd4c71 | |||
| 73cbb27713 | |||
| db2bed16a7 | |||
| ecb5d74d6e | |||
| fea59efe4e | |||
| b43cca5b6f | |||
| 94f562f1bb | |||
| 3d5fc5fee1 | |||
| 98ef751a9f | |||
| f5641ec1f0 | |||
| 0320c3fdd5 | |||
| 3d0ebe4694 | |||
| 510723cac4 | |||
| f6733f87a2 | |||
| ddc02d9bbc | |||
| 659b5bb82b | |||
| 5f72e6f549 |
+3
-2
@@ -22,5 +22,6 @@ node_modules/
|
|||||||
# Environment variables
|
# Environment variables
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Claude
|
# Claude
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
yarn.lock
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"MD013": false
|
||||||
|
}
|
||||||
@@ -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. |
|
||||||
@@ -7,14 +7,11 @@ An immersive RPG extension for browsers that tracks character stats, scene infor
|
|||||||
|
|
||||||
## 🆕 What's New
|
## 🆕 What's New
|
||||||
|
|
||||||
### v3.3.1
|
### DEPRECATED
|
||||||
|
|
||||||
- Thought bubble can now be collapsed into an icon.
|
Moving on to developing the Marinara Engine frontend, the extension will now be maintained by the community!
|
||||||
- Fixed a bug for Past Events being parsed incorrectly.
|
|
||||||
- Added event emission on when the tracker generation is complete.
|
|
||||||
|
|
||||||
**Special thanks to all the other contributors for this project:**
|
<https://github.com/Pasta-Devs/Marinara-Engine>
|
||||||
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
|
|
||||||
|
|
||||||
## 📥 Installation
|
## 📥 Installation
|
||||||
|
|
||||||
@@ -24,7 +21,7 @@ Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungch
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -102,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
|
||||||
|
|
||||||
@@ -130,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
|
||||||
|
|
||||||
@@ -166,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
|
||||||
@@ -202,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:
|
||||||
@@ -267,7 +271,7 @@ If you enjoy this extension, consider supporting development:
|
|||||||
## 🙏 Credits
|
## 🙏 Credits
|
||||||
|
|
||||||
**Contributors:**
|
**Contributors:**
|
||||||
SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
|
SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein.
|
||||||
|
|
||||||
## 🚀 Planned Features
|
## 🚀 Planned Features
|
||||||
|
|
||||||
@@ -289,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)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getContext, renderExtensionTemplateAsync, extension_settings as st_extension_settings } from '../../../extensions.js';
|
import { getContext, renderExtensionTemplateAsync, extension_settings as st_extension_settings } from '../../../extensions.js';
|
||||||
import { eventSource, event_types, substituteParams, chat, generateRaw, saveSettingsDebounced, chat_metadata, saveChatDebounced, user_avatar, getThumbnailUrl, characters, this_chid, extension_prompt_types, extension_prompt_roles, setExtensionPrompt, reloadCurrentChat, Generate, getRequestHeaders } from '../../../../script.js';
|
import { eventSource, event_types, substituteParams, chat, saveSettingsDebounced, chat_metadata, saveChatDebounced, user_avatar, getThumbnailUrl, characters, this_chid, extension_prompt_types, extension_prompt_roles, setExtensionPrompt, reloadCurrentChat, Generate, getRequestHeaders } from '../../../../script.js';
|
||||||
import { selected_group, getGroupMembers } from '../../../group-chats.js';
|
import { selected_group, getGroupMembers } from '../../../group-chats.js';
|
||||||
import { power_user } from '../../../power-user.js';
|
import { power_user } from '../../../power-user.js';
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
$infoBoxContainer,
|
$infoBoxContainer,
|
||||||
$thoughtsContainer,
|
$thoughtsContainer,
|
||||||
$inventoryContainer,
|
$inventoryContainer,
|
||||||
|
$equipmentContainer,
|
||||||
$questsContainer,
|
$questsContainer,
|
||||||
$musicPlayerContainer,
|
$musicPlayerContainer,
|
||||||
setExtensionSettings,
|
setExtensionSettings,
|
||||||
@@ -38,11 +39,12 @@ import {
|
|||||||
setInfoBoxContainer,
|
setInfoBoxContainer,
|
||||||
setThoughtsContainer,
|
setThoughtsContainer,
|
||||||
setInventoryContainer,
|
setInventoryContainer,
|
||||||
|
setEquipmentContainer,
|
||||||
setQuestsContainer,
|
setQuestsContainer,
|
||||||
setMusicPlayerContainer,
|
setMusicPlayerContainer,
|
||||||
clearSessionAvatarPrompts
|
clearSessionAvatarPrompts
|
||||||
} from './src/core/state.js';
|
} from './src/core/state.js';
|
||||||
import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData } from './src/core/persistence.js';
|
import { loadSettings, saveSettings, saveChatData, loadChatData, updateMessageSwipeData, commitTrackerDataFromPriorMessage } from './src/core/persistence.js';
|
||||||
import { registerAllEvents } from './src/core/events.js';
|
import { registerAllEvents } from './src/core/events.js';
|
||||||
|
|
||||||
// Generation & Parsing modules
|
// Generation & Parsing modules
|
||||||
@@ -64,10 +66,12 @@ import { renderInfoBox, updateInfoBoxField } from './src/systems/rendering/infoB
|
|||||||
import {
|
import {
|
||||||
renderThoughts,
|
renderThoughts,
|
||||||
updateCharacterField,
|
updateCharacterField,
|
||||||
|
removeCharacter,
|
||||||
updateChatThoughts,
|
updateChatThoughts,
|
||||||
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';
|
||||||
@@ -75,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 {
|
||||||
@@ -94,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
|
||||||
@@ -125,12 +131,28 @@ import {
|
|||||||
removeMobileTabs,
|
removeMobileTabs,
|
||||||
setupMobileKeyboardHandling,
|
setupMobileKeyboardHandling,
|
||||||
setupContentEditableScrolling,
|
setupContentEditableScrolling,
|
||||||
updateMobileTabLabels
|
updateMobileTabLabels,
|
||||||
|
updateFabWidgets
|
||||||
} from './src/systems/ui/mobile.js';
|
} from './src/systems/ui/mobile.js';
|
||||||
import {
|
import {
|
||||||
setupDesktopTabs,
|
setupDesktopTabs,
|
||||||
removeDesktopTabs
|
removeDesktopTabs,
|
||||||
|
updateStripWidgets
|
||||||
} from './src/systems/ui/desktop.js';
|
} from './src/systems/ui/desktop.js';
|
||||||
|
import {
|
||||||
|
removeAlternatePresentCharactersPanel,
|
||||||
|
renderAlternatePresentCharacters
|
||||||
|
} from './src/systems/ui/alternatePresentCharacters.js';
|
||||||
|
import {
|
||||||
|
initThoughtBasedExpressions,
|
||||||
|
queueThoughtBasedExpressionsUpdate,
|
||||||
|
onThoughtBasedExpressionsSettingChanged,
|
||||||
|
onAlternatePresentCharactersVisibilityChanged,
|
||||||
|
onHideDefaultExpressionDisplaySettingChanged,
|
||||||
|
clearThoughtBasedExpressionsCache,
|
||||||
|
onThoughtBasedExpressionsChatChanged,
|
||||||
|
setThoughtBasedExpressionsRefreshHandler
|
||||||
|
} from './src/systems/integration/thoughtBasedExpressions.js';
|
||||||
|
|
||||||
// Feature modules
|
// Feature modules
|
||||||
import { setupPlotButtons, sendPlotProgression } from './src/systems/features/plotProgression.js';
|
import { setupPlotButtons, sendPlotProgression } from './src/systems/features/plotProgression.js';
|
||||||
@@ -147,15 +169,23 @@ import {
|
|||||||
onMessageSent,
|
onMessageSent,
|
||||||
onMessageReceived,
|
onMessageReceived,
|
||||||
onCharacterChanged,
|
onCharacterChanged,
|
||||||
|
onChatLoaded,
|
||||||
|
onMessageDeleted,
|
||||||
onMessageSwiped,
|
onMessageSwiped,
|
||||||
|
scheduleChatStateRehydration,
|
||||||
updatePersonaAvatar,
|
updatePersonaAvatar,
|
||||||
clearExtensionPrompts,
|
clearExtensionPrompts,
|
||||||
onGenerationEnded
|
onGenerationEnded,
|
||||||
|
initHistoryInjection
|
||||||
} from './src/systems/integration/sillytavern.js';
|
} from './src/systems/integration/sillytavern.js';
|
||||||
|
|
||||||
// Old state variable declarations removed - now imported from core modules
|
// Old state variable declarations removed - now imported from core modules
|
||||||
// (extensionSettings, lastGeneratedData, committedTrackerData, etc. are now in src/core/state.js)
|
// (extensionSettings, lastGeneratedData, committedTrackerData, etc. are now in src/core/state.js)
|
||||||
|
|
||||||
|
setThoughtBasedExpressionsRefreshHandler(() => {
|
||||||
|
renderAlternatePresentCharacters({ useCommittedFallback: true });
|
||||||
|
});
|
||||||
|
|
||||||
// Utility functions removed - now imported from src/utils/avatars.js
|
// Utility functions removed - now imported from src/utils/avatars.js
|
||||||
// (getSafeThumbnailUrl)
|
// (getSafeThumbnailUrl)
|
||||||
|
|
||||||
@@ -212,6 +242,7 @@ async function addExtensionSettings() {
|
|||||||
clearExtensionPrompts();
|
clearExtensionPrompts();
|
||||||
updateChatThoughts(); // Remove thought bubbles
|
updateChatThoughts(); // Remove thought bubbles
|
||||||
cleanupCheckpointUI(); // Remove checkpoint buttons and indicators
|
cleanupCheckpointUI(); // Remove checkpoint buttons and indicators
|
||||||
|
clearThoughtBasedExpressionsCache();
|
||||||
|
|
||||||
// Disable dynamic weather effects
|
// Disable dynamic weather effects
|
||||||
toggleDynamicWeather(false);
|
toggleDynamicWeather(false);
|
||||||
@@ -221,10 +252,13 @@ async function addExtensionSettings() {
|
|||||||
$('#rpg-mobile-toggle').remove();
|
$('#rpg-mobile-toggle').remove();
|
||||||
$('#rpg-collapse-toggle').remove();
|
$('#rpg-collapse-toggle').remove();
|
||||||
$('#rpg-plot-buttons').remove(); // Remove plot buttons
|
$('#rpg-plot-buttons').remove(); // Remove plot buttons
|
||||||
|
removeAlternatePresentCharactersPanel();
|
||||||
} else if (extensionSettings.enabled && !wasEnabled) {
|
} else if (extensionSettings.enabled && !wasEnabled) {
|
||||||
// Enabling extension - initialize UI
|
// Enabling extension - initialize UI
|
||||||
await initUI();
|
await initUI();
|
||||||
loadChatData(); // Load chat data for current chat
|
loadChatData(); // Load chat data for current chat
|
||||||
|
scheduleChatStateRehydration();
|
||||||
|
initThoughtBasedExpressions();
|
||||||
updateChatThoughts(); // Create thought bubbles if data exists
|
updateChatThoughts(); // Create thought bubbles if data exists
|
||||||
injectCheckpointButton(); // Re-add checkpoint buttons
|
injectCheckpointButton(); // Re-add checkpoint buttons
|
||||||
updateAllCheckpointIndicators(); // Update button states
|
updateAllCheckpointIndicators(); // Update button states
|
||||||
@@ -283,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'));
|
||||||
|
|
||||||
@@ -331,6 +366,26 @@ async function initUI() {
|
|||||||
extensionSettings.showCharacterThoughts = $(this).prop('checked');
|
extensionSettings.showCharacterThoughts = $(this).prop('checked');
|
||||||
saveSettings();
|
saveSettings();
|
||||||
updateSectionVisibility();
|
updateSectionVisibility();
|
||||||
|
renderThoughts();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-alt-present-characters').on('change', function() {
|
||||||
|
extensionSettings.showAlternatePresentCharactersPanel = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
renderThoughts();
|
||||||
|
onAlternatePresentCharactersVisibilityChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-thought-based-expressions').on('change', function() {
|
||||||
|
extensionSettings.enableThoughtBasedExpressions = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
onThoughtBasedExpressionsSettingChanged(extensionSettings.enableThoughtBasedExpressions);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-hide-default-expressions').on('change', function() {
|
||||||
|
extensionSettings.hideDefaultExpressionDisplay = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
onHideDefaultExpressionDisplaySettingChanged(extensionSettings.hideDefaultExpressionDisplay);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#rpg-toggle-inventory').on('change', function() {
|
$('#rpg-toggle-inventory').on('change', function() {
|
||||||
@@ -339,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();
|
||||||
@@ -353,6 +414,7 @@ async function initUI() {
|
|||||||
renderInfoBox();
|
renderInfoBox();
|
||||||
renderThoughts();
|
renderThoughts();
|
||||||
renderInventory();
|
renderInventory();
|
||||||
|
renderEquipment();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -363,6 +425,12 @@ async function initUI() {
|
|||||||
updateChatThoughts();
|
updateChatThoughts();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-inline-thoughts').on('change', function() {
|
||||||
|
extensionSettings.thoughtsInChatStyle = $(this).prop('checked') ? 'inline' : 'corner';
|
||||||
|
saveSettings();
|
||||||
|
updateChatThoughts();
|
||||||
|
});
|
||||||
|
|
||||||
$('#rpg-toggle-html-prompt').on('change', function() {
|
$('#rpg-toggle-html-prompt').on('change', function() {
|
||||||
extensionSettings.enableHtmlPrompt = $(this).prop('checked');
|
extensionSettings.enableHtmlPrompt = $(this).prop('checked');
|
||||||
// console.log('[RPG Companion] Toggle enableHtmlPrompt changed to:', extensionSettings.enableHtmlPrompt);
|
// console.log('[RPG Companion] Toggle enableHtmlPrompt changed to:', extensionSettings.enableHtmlPrompt);
|
||||||
@@ -375,6 +443,21 @@ async function initUI() {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-deception').on('change', function() {
|
||||||
|
extensionSettings.enableDeceptionSystem = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-omniscience').on('change', function() {
|
||||||
|
extensionSettings.enableOmniscienceFilter = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-cyoa').on('change', function() {
|
||||||
|
extensionSettings.enableCYOA = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
$('#rpg-toggle-spotify-music').on('change', function() {
|
$('#rpg-toggle-spotify-music').on('change', function() {
|
||||||
extensionSettings.enableSpotifyMusic = $(this).prop('checked');
|
extensionSettings.enableSpotifyMusic = $(this).prop('checked');
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -552,6 +635,24 @@ async function initUI() {
|
|||||||
updateFeatureTogglesVisibility();
|
updateFeatureTogglesVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-show-deception-toggle').on('change', function() {
|
||||||
|
extensionSettings.showDeceptionToggle = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFeatureTogglesVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-show-omniscience-toggle').on('change', function() {
|
||||||
|
extensionSettings.showOmniscienceToggle = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFeatureTogglesVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-show-cyoa-toggle').on('change', function() {
|
||||||
|
extensionSettings.showCYOAToggle = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFeatureTogglesVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
$('#rpg-toggle-show-spotify-toggle').on('change', function() {
|
$('#rpg-toggle-show-spotify-toggle').on('change', function() {
|
||||||
extensionSettings.showSpotifyToggle = $(this).prop('checked');
|
extensionSettings.showSpotifyToggle = $(this).prop('checked');
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -568,6 +669,34 @@ async function initUI() {
|
|||||||
}
|
}
|
||||||
saveSettings();
|
saveSettings();
|
||||||
updateFeatureTogglesVisibility();
|
updateFeatureTogglesVisibility();
|
||||||
|
updateWeatherSubOptionsVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Weather sub-options (background and foreground) - radio buttons
|
||||||
|
$('#rpg-toggle-weather-background').on('change', function() {
|
||||||
|
if ($(this).prop('checked')) {
|
||||||
|
extensionSettings.weatherBackground = true;
|
||||||
|
extensionSettings.weatherForeground = false;
|
||||||
|
saveSettings();
|
||||||
|
// Re-apply weather effect
|
||||||
|
if (extensionSettings.enableDynamicWeather) {
|
||||||
|
toggleDynamicWeather(false);
|
||||||
|
toggleDynamicWeather(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-weather-foreground').on('change', function() {
|
||||||
|
if ($(this).prop('checked')) {
|
||||||
|
extensionSettings.weatherBackground = false;
|
||||||
|
extensionSettings.weatherForeground = true;
|
||||||
|
saveSettings();
|
||||||
|
// Re-apply weather effect
|
||||||
|
if (extensionSettings.enableDynamicWeather) {
|
||||||
|
toggleDynamicWeather(false);
|
||||||
|
toggleDynamicWeather(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#rpg-toggle-show-narrator-mode').on('change', function() {
|
$('#rpg-toggle-show-narrator-mode').on('change', function() {
|
||||||
@@ -607,12 +736,164 @@ async function initUI() {
|
|||||||
updateDiceDisplay();
|
updateDiceDisplay();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mobile FAB Widget toggles - simplified, no position saving (auto-positioned)
|
||||||
|
$('#rpg-toggle-fab-widgets-enabled').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
extensionSettings.mobileFabWidgets.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
$('#rpg-fab-widget-options').toggle(extensionSettings.mobileFabWidgets.enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-weather-icon').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.weatherIcon) extensionSettings.mobileFabWidgets.weatherIcon = {};
|
||||||
|
extensionSettings.mobileFabWidgets.weatherIcon.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-weather-desc').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.weatherDesc) extensionSettings.mobileFabWidgets.weatherDesc = {};
|
||||||
|
extensionSettings.mobileFabWidgets.weatherDesc.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-clock').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.clock) extensionSettings.mobileFabWidgets.clock = {};
|
||||||
|
extensionSettings.mobileFabWidgets.clock.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-date').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.date) extensionSettings.mobileFabWidgets.date = {};
|
||||||
|
extensionSettings.mobileFabWidgets.date.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-location').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.location) extensionSettings.mobileFabWidgets.location = {};
|
||||||
|
extensionSettings.mobileFabWidgets.location.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-stats').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.stats) extensionSettings.mobileFabWidgets.stats = {};
|
||||||
|
extensionSettings.mobileFabWidgets.stats.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-fab-attributes').on('change', function() {
|
||||||
|
if (!extensionSettings.mobileFabWidgets) extensionSettings.mobileFabWidgets = {};
|
||||||
|
if (!extensionSettings.mobileFabWidgets.attributes) extensionSettings.mobileFabWidgets.attributes = {};
|
||||||
|
extensionSettings.mobileFabWidgets.attributes.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateFabWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Desktop Strip Widget toggles
|
||||||
|
$('#rpg-toggle-strip-widgets-enabled').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
extensionSettings.desktopStripWidgets.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
$('#rpg-strip-widget-options').toggle(extensionSettings.desktopStripWidgets.enabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-weather-icon').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.weatherIcon) extensionSettings.desktopStripWidgets.weatherIcon = {};
|
||||||
|
extensionSettings.desktopStripWidgets.weatherIcon.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-clock').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.clock) extensionSettings.desktopStripWidgets.clock = {};
|
||||||
|
extensionSettings.desktopStripWidgets.clock.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-date').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.date) extensionSettings.desktopStripWidgets.date = {};
|
||||||
|
extensionSettings.desktopStripWidgets.date.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-location').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.location) extensionSettings.desktopStripWidgets.location = {};
|
||||||
|
extensionSettings.desktopStripWidgets.location.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-stats').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.stats) extensionSettings.desktopStripWidgets.stats = {};
|
||||||
|
extensionSettings.desktopStripWidgets.stats.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#rpg-toggle-strip-attributes').on('change', function() {
|
||||||
|
if (!extensionSettings.desktopStripWidgets) extensionSettings.desktopStripWidgets = {};
|
||||||
|
if (!extensionSettings.desktopStripWidgets.attributes) extensionSettings.desktopStripWidgets.attributes = {};
|
||||||
|
extensionSettings.desktopStripWidgets.attributes.enabled = $(this).prop('checked');
|
||||||
|
saveSettings();
|
||||||
|
updateStripWidgets();
|
||||||
|
});
|
||||||
|
|
||||||
$('#rpg-manual-update').on('click', async function() {
|
$('#rpg-manual-update').on('click', async function() {
|
||||||
if (!extensionSettings.enabled) {
|
if (!extensionSettings.enabled) {
|
||||||
// console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.');
|
// console.log('[RPG Companion] Extension is disabled. Please enable it in the Extensions tab.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
const currentChat = getContext().chat;
|
||||||
|
let lastAssistantIndex = -1;
|
||||||
|
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||||||
|
if (!currentChat[i].is_user && !currentChat[i].is_system) {
|
||||||
|
lastAssistantIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastAssistantIndex !== -1) {
|
||||||
|
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
||||||
|
}
|
||||||
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, renderEquipment);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Strip widget refresh button - same functionality as main refresh button
|
||||||
|
$('#rpg-strip-refresh').on('click', async function() {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentChat = getContext().chat;
|
||||||
|
let lastAssistantIndex = -1;
|
||||||
|
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||||||
|
if (!currentChat[i].is_user && !currentChat[i].is_system) {
|
||||||
|
lastAssistantIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastAssistantIndex !== -1) {
|
||||||
|
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
||||||
|
}
|
||||||
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, renderEquipment);
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#rpg-stat-bar-color-low').on('change', function() {
|
$('#rpg-stat-bar-color-low').on('change', function() {
|
||||||
@@ -621,12 +902,30 @@ async function initUI() {
|
|||||||
renderUserStats(); // Re-render with new colors
|
renderUserStats(); // Re-render with new colors
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-stat-bar-color-low-opacity').on('input', function() {
|
||||||
|
const opacity = Number($(this).val());
|
||||||
|
extensionSettings.statBarColorLowOpacity = opacity;
|
||||||
|
$('#rpg-stat-bar-color-low-opacity-value').text(opacity + '%');
|
||||||
|
renderUserStats();
|
||||||
|
}).on('change', function() {
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
$('#rpg-stat-bar-color-high').on('change', function() {
|
$('#rpg-stat-bar-color-high').on('change', function() {
|
||||||
extensionSettings.statBarColorHigh = String($(this).val());
|
extensionSettings.statBarColorHigh = String($(this).val());
|
||||||
saveSettings();
|
saveSettings();
|
||||||
renderUserStats(); // Re-render with new colors
|
renderUserStats(); // Re-render with new colors
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-stat-bar-color-high-opacity').on('input', function() {
|
||||||
|
const opacity = Number($(this).val());
|
||||||
|
extensionSettings.statBarColorHighOpacity = opacity;
|
||||||
|
$('#rpg-stat-bar-color-high-opacity-value').text(opacity + '%');
|
||||||
|
renderUserStats();
|
||||||
|
}).on('change', function() {
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
// Theme selection
|
// Theme selection
|
||||||
$('#rpg-theme-select').on('change', function() {
|
$('#rpg-theme-select').on('change', function() {
|
||||||
extensionSettings.theme = String($(this).val());
|
extensionSettings.theme = String($(this).val());
|
||||||
@@ -648,6 +947,19 @@ async function initUI() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-custom-bg-opacity').on('input', function() {
|
||||||
|
const opacity = Number($(this).val());
|
||||||
|
extensionSettings.customColors.bgOpacity = opacity;
|
||||||
|
$('#rpg-custom-bg-opacity-value').text(opacity + '%');
|
||||||
|
if (extensionSettings.theme === 'custom') {
|
||||||
|
applyCustomTheme();
|
||||||
|
updateSettingsPopupTheme(getSettingsModal());
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
}).on('change', function() {
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
$('#rpg-custom-accent').on('change', function() {
|
$('#rpg-custom-accent').on('change', function() {
|
||||||
extensionSettings.customColors.accent = String($(this).val());
|
extensionSettings.customColors.accent = String($(this).val());
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -658,6 +970,19 @@ async function initUI() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-custom-accent-opacity').on('input', function() {
|
||||||
|
const opacity = Number($(this).val());
|
||||||
|
extensionSettings.customColors.accentOpacity = opacity;
|
||||||
|
$('#rpg-custom-accent-opacity-value').text(opacity + '%');
|
||||||
|
if (extensionSettings.theme === 'custom') {
|
||||||
|
applyCustomTheme();
|
||||||
|
updateSettingsPopupTheme(getSettingsModal());
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
}).on('change', function() {
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
$('#rpg-custom-text').on('change', function() {
|
$('#rpg-custom-text').on('change', function() {
|
||||||
extensionSettings.customColors.text = String($(this).val());
|
extensionSettings.customColors.text = String($(this).val());
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -668,6 +993,19 @@ async function initUI() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-custom-text-opacity').on('input', function() {
|
||||||
|
const opacity = Number($(this).val());
|
||||||
|
extensionSettings.customColors.textOpacity = opacity;
|
||||||
|
$('#rpg-custom-text-opacity-value').text(opacity + '%');
|
||||||
|
if (extensionSettings.theme === 'custom') {
|
||||||
|
applyCustomTheme();
|
||||||
|
updateSettingsPopupTheme(getSettingsModal());
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
}).on('change', function() {
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
$('#rpg-custom-highlight').on('change', function() {
|
$('#rpg-custom-highlight').on('change', function() {
|
||||||
extensionSettings.customColors.highlight = String($(this).val());
|
extensionSettings.customColors.highlight = String($(this).val());
|
||||||
saveSettings();
|
saveSettings();
|
||||||
@@ -678,6 +1016,19 @@ async function initUI() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#rpg-custom-highlight-opacity').on('input', function() {
|
||||||
|
const opacity = Number($(this).val());
|
||||||
|
extensionSettings.customColors.highlightOpacity = opacity;
|
||||||
|
$('#rpg-custom-highlight-opacity-value').text(opacity + '%');
|
||||||
|
if (extensionSettings.theme === 'custom') {
|
||||||
|
applyCustomTheme();
|
||||||
|
updateSettingsPopupTheme(getSettingsModal());
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
}).on('change', function() {
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
// External API settings event handlers
|
// External API settings event handlers
|
||||||
$('#rpg-external-base-url').on('change', function() {
|
$('#rpg-external-base-url').on('change', function() {
|
||||||
if (!extensionSettings.externalApiSettings) {
|
if (!extensionSettings.externalApiSettings) {
|
||||||
@@ -778,12 +1129,20 @@ async function initUI() {
|
|||||||
$('#rpg-toggle-user-stats').prop('checked', extensionSettings.showUserStats);
|
$('#rpg-toggle-user-stats').prop('checked', extensionSettings.showUserStats);
|
||||||
$('#rpg-toggle-info-box').prop('checked', extensionSettings.showInfoBox);
|
$('#rpg-toggle-info-box').prop('checked', extensionSettings.showInfoBox);
|
||||||
$('#rpg-toggle-thoughts').prop('checked', extensionSettings.showCharacterThoughts);
|
$('#rpg-toggle-thoughts').prop('checked', extensionSettings.showCharacterThoughts);
|
||||||
|
$('#rpg-toggle-alt-present-characters').prop('checked', extensionSettings.showAlternatePresentCharactersPanel ?? false);
|
||||||
|
$('#rpg-toggle-thought-based-expressions').prop('checked', extensionSettings.enableThoughtBasedExpressions === 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);
|
||||||
|
$('#rpg-toggle-inline-thoughts').prop('checked', (extensionSettings.thoughtsInChatStyle || 'corner') === 'inline');
|
||||||
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
|
$('#rpg-toggle-html-prompt').prop('checked', extensionSettings.enableHtmlPrompt);
|
||||||
$('#rpg-toggle-dialogue-coloring').prop('checked', extensionSettings.enableDialogueColoring);
|
$('#rpg-toggle-dialogue-coloring').prop('checked', extensionSettings.enableDialogueColoring);
|
||||||
|
$('#rpg-toggle-deception').prop('checked', extensionSettings.enableDeceptionSystem ?? false);
|
||||||
|
$('#rpg-toggle-omniscience').prop('checked', extensionSettings.enableOmniscienceFilter ?? false);
|
||||||
|
$('#rpg-toggle-cyoa').prop('checked', extensionSettings.enableCYOA ?? false);
|
||||||
$('#rpg-toggle-spotify-music').prop('checked', extensionSettings.enableSpotifyMusic);
|
$('#rpg-toggle-spotify-music').prop('checked', extensionSettings.enableSpotifyMusic);
|
||||||
|
|
||||||
$('#rpg-toggle-dynamic-weather').prop('checked', extensionSettings.enableDynamicWeather);
|
$('#rpg-toggle-dynamic-weather').prop('checked', extensionSettings.enableDynamicWeather);
|
||||||
@@ -792,8 +1151,13 @@ async function initUI() {
|
|||||||
// Feature toggle visibility settings
|
// Feature toggle visibility settings
|
||||||
$('#rpg-toggle-show-html-toggle').prop('checked', extensionSettings.showHtmlToggle ?? true);
|
$('#rpg-toggle-show-html-toggle').prop('checked', extensionSettings.showHtmlToggle ?? true);
|
||||||
$('#rpg-toggle-show-dialogue-coloring-toggle').prop('checked', extensionSettings.showDialogueColoringToggle ?? true);
|
$('#rpg-toggle-show-dialogue-coloring-toggle').prop('checked', extensionSettings.showDialogueColoringToggle ?? true);
|
||||||
|
$('#rpg-toggle-show-deception-toggle').prop('checked', extensionSettings.showDeceptionToggle ?? true);
|
||||||
|
$('#rpg-toggle-show-omniscience-toggle').prop('checked', extensionSettings.showOmniscienceToggle ?? true);
|
||||||
|
$('#rpg-toggle-show-cyoa-toggle').prop('checked', extensionSettings.showCYOAToggle ?? true);
|
||||||
$('#rpg-toggle-show-spotify-toggle').prop('checked', extensionSettings.showSpotifyToggle ?? true);
|
$('#rpg-toggle-show-spotify-toggle').prop('checked', extensionSettings.showSpotifyToggle ?? true);
|
||||||
$('#rpg-toggle-show-dynamic-weather-toggle').prop('checked', extensionSettings.showDynamicWeatherToggle ?? true);
|
$('#rpg-toggle-show-dynamic-weather-toggle').prop('checked', extensionSettings.showDynamicWeatherToggle ?? true);
|
||||||
|
$('#rpg-toggle-weather-background').prop('checked', extensionSettings.weatherBackground ?? true);
|
||||||
|
$('#rpg-toggle-weather-foreground').prop('checked', extensionSettings.weatherForeground ?? false);
|
||||||
$('#rpg-toggle-show-narrator-mode').prop('checked', extensionSettings.showNarratorMode ?? true);
|
$('#rpg-toggle-show-narrator-mode').prop('checked', extensionSettings.showNarratorMode ?? true);
|
||||||
$('#rpg-toggle-show-auto-avatars').prop('checked', extensionSettings.showAutoAvatars ?? true);
|
$('#rpg-toggle-show-auto-avatars').prop('checked', extensionSettings.showAutoAvatars ?? true);
|
||||||
|
|
||||||
@@ -824,13 +1188,56 @@ async function initUI() {
|
|||||||
$('#rpg-toggle-auto-avatars-panel').prop('checked', extensionSettings.autoGenerateAvatars || false);
|
$('#rpg-toggle-auto-avatars-panel').prop('checked', extensionSettings.autoGenerateAvatars || false);
|
||||||
|
|
||||||
$('#rpg-toggle-dice-display').prop('checked', extensionSettings.showDiceDisplay);
|
$('#rpg-toggle-dice-display').prop('checked', extensionSettings.showDiceDisplay);
|
||||||
|
|
||||||
|
// Initialize Mobile FAB Widget checkboxes
|
||||||
|
const fabWidgets = extensionSettings.mobileFabWidgets || {};
|
||||||
|
$('#rpg-toggle-fab-widgets-enabled').prop('checked', fabWidgets.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-weather-icon').prop('checked', fabWidgets.weatherIcon?.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-weather-desc').prop('checked', fabWidgets.weatherDesc?.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-clock').prop('checked', fabWidgets.clock?.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-date').prop('checked', fabWidgets.date?.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-location').prop('checked', fabWidgets.location?.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-stats').prop('checked', fabWidgets.stats?.enabled || false);
|
||||||
|
$('#rpg-toggle-fab-attributes').prop('checked', fabWidgets.attributes?.enabled || false);
|
||||||
|
// Toggle visibility of widget options based on master toggle
|
||||||
|
$('#rpg-fab-widget-options').toggle(fabWidgets.enabled || false);
|
||||||
|
|
||||||
|
// Initialize Desktop Strip Widget checkboxes
|
||||||
|
const stripWidgets = extensionSettings.desktopStripWidgets || {};
|
||||||
|
$('#rpg-toggle-strip-widgets-enabled').prop('checked', stripWidgets.enabled || false);
|
||||||
|
$('#rpg-toggle-strip-weather-icon').prop('checked', stripWidgets.weatherIcon?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-clock').prop('checked', stripWidgets.clock?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-date').prop('checked', stripWidgets.date?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-location').prop('checked', stripWidgets.location?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-stats').prop('checked', stripWidgets.stats?.enabled ?? true);
|
||||||
|
$('#rpg-toggle-strip-attributes').prop('checked', stripWidgets.attributes?.enabled ?? true);
|
||||||
|
// Toggle visibility of strip widget options based on master toggle
|
||||||
|
$('#rpg-strip-widget-options').toggle(stripWidgets.enabled || false);
|
||||||
|
|
||||||
$('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow);
|
$('#rpg-stat-bar-color-low').val(extensionSettings.statBarColorLow);
|
||||||
|
$('#rpg-stat-bar-color-low-opacity').val(extensionSettings.statBarColorLowOpacity ?? 100);
|
||||||
|
$('#rpg-stat-bar-color-low-opacity-value').text((extensionSettings.statBarColorLowOpacity ?? 100) + '%');
|
||||||
|
|
||||||
$('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh);
|
$('#rpg-stat-bar-color-high').val(extensionSettings.statBarColorHigh);
|
||||||
|
$('#rpg-stat-bar-color-high-opacity').val(extensionSettings.statBarColorHighOpacity ?? 100);
|
||||||
|
$('#rpg-stat-bar-color-high-opacity-value').text((extensionSettings.statBarColorHighOpacity ?? 100) + '%');
|
||||||
|
|
||||||
$('#rpg-theme-select').val(extensionSettings.theme);
|
$('#rpg-theme-select').val(extensionSettings.theme);
|
||||||
$('#rpg-custom-bg').val(extensionSettings.customColors.bg);
|
$('#rpg-custom-bg').val(extensionSettings.customColors.bg);
|
||||||
|
$('#rpg-custom-bg-opacity').val(extensionSettings.customColors.bgOpacity ?? 100);
|
||||||
|
$('#rpg-custom-bg-opacity-value').text((extensionSettings.customColors.bgOpacity ?? 100) + '%');
|
||||||
|
|
||||||
$('#rpg-custom-accent').val(extensionSettings.customColors.accent);
|
$('#rpg-custom-accent').val(extensionSettings.customColors.accent);
|
||||||
|
$('#rpg-custom-accent-opacity').val(extensionSettings.customColors.accentOpacity ?? 100);
|
||||||
|
$('#rpg-custom-accent-opacity-value').text((extensionSettings.customColors.accentOpacity ?? 100) + '%');
|
||||||
|
|
||||||
$('#rpg-custom-text').val(extensionSettings.customColors.text);
|
$('#rpg-custom-text').val(extensionSettings.customColors.text);
|
||||||
|
$('#rpg-custom-text-opacity').val(extensionSettings.customColors.textOpacity ?? 100);
|
||||||
|
$('#rpg-custom-text-opacity-value').text((extensionSettings.customColors.textOpacity ?? 100) + '%');
|
||||||
|
|
||||||
$('#rpg-custom-highlight').val(extensionSettings.customColors.highlight);
|
$('#rpg-custom-highlight').val(extensionSettings.customColors.highlight);
|
||||||
|
$('#rpg-custom-highlight-opacity').val(extensionSettings.customColors.highlightOpacity ?? 100);
|
||||||
|
$('#rpg-custom-highlight-opacity-value').text((extensionSettings.customColors.highlightOpacity ?? 100) + '%');
|
||||||
|
|
||||||
// Initialize External API settings values
|
// Initialize External API settings values
|
||||||
if (extensionSettings.externalApiSettings) {
|
if (extensionSettings.externalApiSettings) {
|
||||||
@@ -847,7 +1254,6 @@ async function initUI() {
|
|||||||
|
|
||||||
$('#rpg-generation-mode').val(extensionSettings.generationMode);
|
$('#rpg-generation-mode').val(extensionSettings.generationMode);
|
||||||
$('#rpg-skip-guided-mode').val(extensionSettings.skipInjectionsForGuided);
|
$('#rpg-skip-guided-mode').val(extensionSettings.skipInjectionsForGuided);
|
||||||
$('#rpg-save-tracker-history').prop('checked', extensionSettings.saveTrackerHistory);
|
|
||||||
|
|
||||||
updatePanelVisibility();
|
updatePanelVisibility();
|
||||||
updateSectionVisibility();
|
updateSectionVisibility();
|
||||||
@@ -878,6 +1284,7 @@ async function initUI() {
|
|||||||
renderInfoBox();
|
renderInfoBox();
|
||||||
renderThoughts();
|
renderThoughts();
|
||||||
renderInventory();
|
renderInventory();
|
||||||
|
renderEquipment();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
updateDiceDisplay();
|
updateDiceDisplay();
|
||||||
@@ -891,6 +1298,7 @@ async function initUI() {
|
|||||||
setupMobileKeyboardHandling();
|
setupMobileKeyboardHandling();
|
||||||
setupContentEditableScrolling();
|
setupContentEditableScrolling();
|
||||||
initInventoryEventListeners();
|
initInventoryEventListeners();
|
||||||
|
initEquipmentEventListeners();
|
||||||
|
|
||||||
// Initialize chapter checkpoint UI
|
// Initialize chapter checkpoint UI
|
||||||
initChapterCheckpointUI();
|
initChapterCheckpointUI();
|
||||||
@@ -968,6 +1376,11 @@ jQuery(async () => {
|
|||||||
// Load chat-specific data for current chat
|
// Load chat-specific data for current chat
|
||||||
try {
|
try {
|
||||||
loadChatData();
|
loadChatData();
|
||||||
|
scheduleChatStateRehydration();
|
||||||
|
initThoughtBasedExpressions();
|
||||||
|
// Initialize FAB widgets and strip widgets with any loaded data
|
||||||
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] Chat data load failed, using defaults:', error);
|
console.error('[RPG Companion] Chat data load failed, using defaults:', error);
|
||||||
}
|
}
|
||||||
@@ -1017,6 +1430,15 @@ jQuery(async () => {
|
|||||||
// Non-critical - continue anyway
|
// Non-critical - continue anyway
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize history injection event listeners
|
||||||
|
// This must be done before event registration so listeners are ready
|
||||||
|
try {
|
||||||
|
initHistoryInjection();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[RPG Companion] History injection init failed:', error);
|
||||||
|
// Non-critical - continue without it
|
||||||
|
}
|
||||||
|
|
||||||
// Register all event listeners
|
// Register all event listeners
|
||||||
try {
|
try {
|
||||||
registerAllEvents({
|
registerAllEvents({
|
||||||
@@ -1026,10 +1448,69 @@ jQuery(async () => {
|
|||||||
[event_types.GENERATION_STOPPED]: onGenerationEnded,
|
[event_types.GENERATION_STOPPED]: onGenerationEnded,
|
||||||
[event_types.GENERATION_ENDED]: onGenerationEnded,
|
[event_types.GENERATION_ENDED]: onGenerationEnded,
|
||||||
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts],
|
[event_types.CHAT_CHANGED]: [onCharacterChanged, updatePersonaAvatar, restoreCheckpointOnLoad, clearSessionAvatarPrompts],
|
||||||
|
[event_types.CHAT_LOADED]: onChatLoaded,
|
||||||
|
[event_types.MESSAGE_DELETED]: onMessageDeleted,
|
||||||
|
[event_types.MESSAGE_SWIPE_DELETED]: onMessageDeleted,
|
||||||
[event_types.MESSAGE_SWIPED]: onMessageSwiped,
|
[event_types.MESSAGE_SWIPED]: onMessageSwiped,
|
||||||
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
|
[event_types.USER_MESSAGE_RENDERED]: updatePersonaAvatar,
|
||||||
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar
|
[event_types.SETTINGS_UPDATED]: updatePersonaAvatar
|
||||||
});
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (messageId) => {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedMessage = chat[messageId];
|
||||||
|
if (renderedMessage && !renderedMessage.is_user && !renderedMessage.is_system) {
|
||||||
|
queueThoughtBasedExpressionsUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.MESSAGE_UPDATED, (messageId) => {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedMessage = chat[messageId];
|
||||||
|
if (updatedMessage && !updatedMessage.is_user && !updatedMessage.is_system) {
|
||||||
|
queueThoughtBasedExpressionsUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.MESSAGE_SWIPED, (messageIndex) => {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipedMessage = chat[messageIndex];
|
||||||
|
if (swipedMessage && !swipedMessage.is_user && !swipedMessage.is_system) {
|
||||||
|
queueThoughtBasedExpressionsUpdate({ immediate: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||||||
|
clearThoughtBasedExpressionsCache();
|
||||||
|
setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.MESSAGE_DELETED, () => {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearThoughtBasedExpressionsCache();
|
||||||
|
setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.on(event_types.MESSAGE_SWIPE_DELETED, () => {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearThoughtBasedExpressionsCache();
|
||||||
|
setTimeout(() => onThoughtBasedExpressionsChatChanged(), 0);
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] Event registration failed:', error);
|
console.error('[RPG Companion] Event registration failed:', error);
|
||||||
throw error; // This is critical - can't continue without events
|
throw error; // This is critical - can't continue without events
|
||||||
@@ -1046,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1067,3 +1551,17 @@ jQuery(async () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the visibility of weather sub-options in settings based on dynamic weather toggle
|
||||||
|
*/
|
||||||
|
function updateWeatherSubOptionsVisibility() {
|
||||||
|
const $weatherSubOptions = $('#rpg-weather-suboptions');
|
||||||
|
const isDynamicWeatherEnabled = extensionSettings.showDynamicWeatherToggle ?? true;
|
||||||
|
|
||||||
|
if (isDynamicWeatherEnabled) {
|
||||||
|
$weatherSubOptions.show();
|
||||||
|
} else {
|
||||||
|
$weatherSubOptions.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
"js": "index.js",
|
"js": "index.js",
|
||||||
"css": "style.css",
|
"css": "style.css",
|
||||||
"author": "Marinara",
|
"author": "Marinara",
|
||||||
"version": "3.3.1",
|
"version": "3.7.4",
|
||||||
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
"homePage": "https://github.com/SpicyMarinara/rpg-companion-sillytavern"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "rpg-companion-sillytavern",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "rpg-complanion-sillytavern",
|
||||||
|
"version": "3.7.4",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"validate_locale": "node src/i18n/validator.js --watch",
|
||||||
|
"validate_locale_once": "node src/i18n/validator.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"chokidar": "^5.0.0",
|
||||||
|
"fs-extra": "^11.3.3",
|
||||||
|
"glob": "^13.0.6"
|
||||||
|
},
|
||||||
|
"dependencies": {}
|
||||||
|
}
|
||||||
+18
-8
@@ -14,27 +14,36 @@
|
|||||||
<label for="rpg-companion-language-select" data-i18n-key="settings.language.label">Language</label>
|
<label for="rpg-companion-language-select" data-i18n-key="settings.language.label">Language</label>
|
||||||
<select id="rpg-companion-language-select" class="text_pole">
|
<select id="rpg-companion-language-select" class="text_pole">
|
||||||
<option value="en" data-i18n-key="settings.language.option.en">English</option>
|
<option value="en" data-i18n-key="settings.language.option.en">English</option>
|
||||||
|
<option value="zh-cn" data-i18n-key="settings.language.option.zh-cn">简体中文</option>
|
||||||
<option value="zh-tw" data-i18n-key="settings.language.option.zh-tw">繁體中文</option>
|
<option value="zh-tw" data-i18n-key="settings.language.option.zh-tw">繁體中文</option>
|
||||||
|
<option value="ru" data-i18n-key="settings.language.option.ru">Русский</option>
|
||||||
|
<option value="fr" data-i18n-key="settings.language.option.fr">Français</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small class="notes" data-i18n-key="settings.note">Toggle to enable/disable the RPG Companion extension. Configure additional settings within the panel itself.</small>
|
<small class="notes" data-i18n-key="settings.note">Toggle to enable/disable the RPG Companion extension.
|
||||||
|
Configure additional settings within the panel itself.</small>
|
||||||
|
|
||||||
<div style="margin-top: 10px; display: flex; gap: 10px;">
|
<div style="margin-top: 10px; display: flex; gap: 10px;">
|
||||||
<a href="https://discord.com/invite/KdAkTg94ME" target="_blank" class="menu_button" style="flex: 1; text-align: center; text-decoration: none;">
|
<a href="https://discord.com/invite/KdAkTg94ME" target="_blank" class="menu_button"
|
||||||
|
style="flex: 1; text-align: center; text-decoration: none;">
|
||||||
<i class="fa-brands fa-discord"></i> Discord
|
<i class="fa-brands fa-discord"></i> Discord
|
||||||
</a>
|
</a>
|
||||||
<a href="https://ko-fi.com/marinara_spaghetti" target="_blank" class="menu_button" style="flex: 1; text-align: center; text-decoration: none;">
|
<a href="https://ko-fi.com/marinara_spaghetti" target="_blank" class="menu_button"
|
||||||
|
style="flex: 1; text-align: center; text-decoration: none;">
|
||||||
<i class="fa-solid fa-heart"></i> Support
|
<i class="fa-solid fa-heart"></i> Support
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 15px; text-align: center; opacity: 0.7; font-size: 0.8em; line-height: 1.5;">
|
<div style="margin-top: 15px; text-align: center; opacity: 0.7; font-size: 0.8em; line-height: 1.5;">
|
||||||
<div style="margin-bottom: 5px;">
|
<div style="margin-bottom: 5px;">
|
||||||
<i class="fa-solid fa-microchip"></i> <strong data-i18n="settings.recommendedModels.title">Recommended Models:</strong>
|
<i class="fa-solid fa-microchip"></i> <strong
|
||||||
|
data-i18n="settings.recommendedModels.title">Recommended Models:</strong>
|
||||||
</div>
|
</div>
|
||||||
<div style="opacity: 0.8; font-size: 0.9em;" data-i18n="settings.recommendedModels.description">
|
<div style="opacity: 0.8; font-size: 0.9em;" data-i18n="settings.recommendedModels.description">
|
||||||
For the extension to work properly, <strong>it is not recommended to use any models below 20B, especially if they're old.</strong> It works best with the SOTA models such as Deepseek, Claude, GPT, or Gemini.
|
For the extension to work properly, <strong>it is not recommended to use any models below 20B,
|
||||||
|
especially if they're old.</strong> It works best with the SOTA models such as Deepseek, Claude,
|
||||||
|
GPT, or Gemini.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -43,13 +52,14 @@
|
|||||||
<i class="fa-solid fa-users"></i> <strong>Contributors:</strong>
|
<i class="fa-solid fa-users"></i> <strong>Contributors:</strong>
|
||||||
</div>
|
</div>
|
||||||
<div style="opacity: 0.8; font-size: 0.9em;">
|
<div style="opacity: 0.8; font-size: 0.9em;">
|
||||||
SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
|
SpicyMarinara, Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude, IDeathByte,
|
||||||
|
Chungchandev, Joenunezb, Amauragis, Tomt610, and Jakstein.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
|
<div style="margin-top: 10px; text-align: center; opacity: 0.6; font-size: 0.85em;">
|
||||||
v3.3.1
|
v3.7.2
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,10 +29,14 @@ export const defaultSettings = {
|
|||||||
showUserStats: true,
|
showUserStats: true,
|
||||||
showInfoBox: true,
|
showInfoBox: true,
|
||||||
showCharacterThoughts: true,
|
showCharacterThoughts: true,
|
||||||
|
showAlternatePresentCharactersPanel: false,
|
||||||
|
enableThoughtBasedExpressions: false,
|
||||||
|
hideDefaultExpressionDisplay: false,
|
||||||
showInventory: true, // Show inventory section (v2 system)
|
showInventory: true, // Show inventory section (v2 system)
|
||||||
showQuests: true, // Show quests section
|
showQuests: true, // Show quests section
|
||||||
showLockIcons: true, // Show lock/unlock icons on tracker items
|
showLockIcons: true, // Show lock/unlock icons on tracker items
|
||||||
showThoughtsInChat: true, // Show thoughts overlay in chat
|
showThoughtsInChat: true, // Show thoughts overlay in chat
|
||||||
|
thoughtsInChatStyle: 'corner', // 'corner' or 'inline'
|
||||||
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
|
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
|
||||||
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
|
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
|
||||||
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
||||||
|
|||||||
+1311
-25
File diff suppressed because it is too large
Load Diff
+195
-44
@@ -10,7 +10,7 @@
|
|||||||
* Extension settings - persisted to SillyTavern settings
|
* Extension settings - persisted to SillyTavern settings
|
||||||
*/
|
*/
|
||||||
export let extensionSettings = {
|
export let extensionSettings = {
|
||||||
settingsVersion: 3, // Version number for settings migrations (v3 = JSON format)
|
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
|
||||||
@@ -18,22 +18,39 @@ export let extensionSettings = {
|
|||||||
showUserStats: true,
|
showUserStats: true,
|
||||||
showInfoBox: true,
|
showInfoBox: true,
|
||||||
showCharacterThoughts: true,
|
showCharacterThoughts: true,
|
||||||
|
showAlternatePresentCharactersPanel: false,
|
||||||
|
enableThoughtBasedExpressions: 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'
|
||||||
narratorMode: false, // Use character card as narrator instead of fixed character references
|
narratorMode: false, // Use character card as narrator instead of fixed character references
|
||||||
customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default)
|
customNarratorPrompt: '', // Custom narrator mode prompt text (empty = use default)
|
||||||
|
customContextInstructionsPrompt: '', // Custom context instructions prompt text (empty = use default)
|
||||||
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
|
enableHtmlPrompt: false, // Enable immersive HTML prompt injection
|
||||||
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
|
customHtmlPrompt: '', // Custom HTML prompt text (empty = use default)
|
||||||
enableDialogueColoring: false, // Enable dialogue coloring prompt injection
|
enableDialogueColoring: false, // Enable dialogue coloring prompt injection
|
||||||
customDialogueColoringPrompt: '', // Custom dialogue coloring prompt text (empty = use default)
|
customDialogueColoringPrompt: '', // Custom dialogue coloring prompt text (empty = use default)
|
||||||
|
enableDeceptionSystem: false, // Enable deception tracking with <lie> tags
|
||||||
|
customDeceptionPrompt: '', // Custom deception prompt text (empty = use default)
|
||||||
|
enableOmniscienceFilter: false, // Enable omniscience filter with <ofilter> tags
|
||||||
|
customOmnisciencePrompt: '', // Custom omniscience filter prompt text (empty = use default)
|
||||||
|
enableCYOA: false, // Enable "Choose Your Own Adventure" formatting with action choices
|
||||||
|
customCYOAPrompt: '', // Custom CYOA prompt text (empty = use default)
|
||||||
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
|
enableSpotifyMusic: false, // Enable Spotify music integration (asks AI for Spotify URLs)
|
||||||
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
customSpotifyPrompt: '', // Custom Spotify prompt text (empty = use default)
|
||||||
|
|
||||||
enableDynamicWeather: true, // Enable dynamic weather effects based on Info Box weather field (v2: enabled by default)
|
enableDynamicWeather: true, // Enable dynamic weather effects based on Info Box weather field (v2: enabled by default)
|
||||||
|
weatherBackground: true, // Show weather effects in background (behind chat)
|
||||||
|
weatherForeground: false, // Show weather effects in foreground (on top of chat)
|
||||||
dismissedHolidayPromo: false, // User dismissed the holiday promotion banner
|
dismissedHolidayPromo: false, // User dismissed the holiday promotion banner
|
||||||
showHtmlToggle: true, // Show Immersive HTML toggle in main panel
|
showHtmlToggle: true, // Show Immersive HTML toggle in main panel
|
||||||
showDialogueColoringToggle: true, // Show Dialogue Coloring toggle in main panel (enabled by default)
|
showDialogueColoringToggle: true, // Show Dialogue Coloring toggle in main panel (enabled by default)
|
||||||
|
showDeceptionToggle: true, // Show Deception System toggle in main panel
|
||||||
|
showOmniscienceToggle: true, // Show Omniscience Filter toggle in main panel
|
||||||
|
showCYOAToggle: true, // Show CYOA toggle in main panel
|
||||||
showSpotifyToggle: true, // Show Spotify Music toggle in main panel
|
showSpotifyToggle: true, // Show Spotify Music toggle in main panel
|
||||||
|
|
||||||
showDynamicWeatherToggle: true, // Show Dynamic Weather Effects toggle in main panel
|
showDynamicWeatherToggle: true, // Show Dynamic Weather Effects toggle in main panel
|
||||||
@@ -42,43 +59,97 @@ export let extensionSettings = {
|
|||||||
skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility)
|
skipInjectionsForGuided: 'none', // skip injections for instruct injections and quiet prompts (GuidedGenerations compatibility)
|
||||||
enableRandomizedPlot: true, // Show randomized plot progression button above chat input
|
enableRandomizedPlot: true, // Show randomized plot progression button above chat input
|
||||||
enableNaturalPlot: true, // Show natural plot progression button above chat input
|
enableNaturalPlot: true, // Show natural plot progression button above chat input
|
||||||
saveTrackerHistory: false, // Save tracker data in chat history for each message
|
// History persistence settings - inject selected tracker data into historical messages
|
||||||
|
historyPersistence: {
|
||||||
|
enabled: false, // Master toggle for history persistence feature
|
||||||
|
messageCount: 5, // Number of messages to include (0 = all available)
|
||||||
|
injectionPosition: 'assistant_message_end', // 'user_message_end', 'assistant_message_end', 'extra_user_message', 'extra_assistant_message'
|
||||||
|
contextPreamble: '', // Optional custom preamble text (empty = use default short one)
|
||||||
|
sendAllEnabledOnRefresh: false // If true, sends all enabled stats from preset instead of only persistInHistory-enabled stats on Refresh RPG Info
|
||||||
|
},
|
||||||
panelPosition: 'right', // 'left', 'right', or 'top'
|
panelPosition: 'right', // 'left', 'right', or 'top'
|
||||||
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
|
theme: 'default', // Theme: default, sci-fi, fantasy, cyberpunk, custom
|
||||||
customColors: {
|
customColors: {
|
||||||
bg: '#1a1a2e',
|
bg: '#1a1a2e',
|
||||||
|
bgOpacity: 100,
|
||||||
accent: '#16213e',
|
accent: '#16213e',
|
||||||
|
accentOpacity: 100,
|
||||||
text: '#eaeaea',
|
text: '#eaeaea',
|
||||||
highlight: '#e94560'
|
textOpacity: 100,
|
||||||
|
highlight: '#e94560',
|
||||||
|
highlightOpacity: 100
|
||||||
},
|
},
|
||||||
statBarColorLow: '#cc3333', // Color for low stat values (red)
|
statBarColorLow: '#cc3333', // Color for low stat values (red)
|
||||||
|
statBarColorLowOpacity: 100,
|
||||||
statBarColorHigh: '#33cc66', // Color for high stat values (green)
|
statBarColorHigh: '#33cc66', // Color for high stat values (green)
|
||||||
|
statBarColorHighOpacity: 100,
|
||||||
enableAnimations: true, // Enable smooth animations for stats and content updates
|
enableAnimations: true, // Enable smooth animations for stats and content updates
|
||||||
mobileFabPosition: {
|
mobileFabPosition: {
|
||||||
top: 'calc(var(--topBarBlockSize) + 60px)',
|
top: 'calc(var(--topBarBlockSize) + 60px)',
|
||||||
right: '12px'
|
right: '12px'
|
||||||
}, // Saved position for mobile FAB button
|
}, // Saved position for mobile FAB button
|
||||||
userStats: JSON.stringify({
|
// Mobile FAB widget display options (8-position system around the button)
|
||||||
stats: [
|
mobileFabWidgets: {
|
||||||
{ id: 'health', name: 'Health', value: 100 },
|
enabled: true, // Master toggle for FAB widgets
|
||||||
{ id: 'satiety', name: 'Satiety', value: 100 },
|
weatherIcon: { enabled: true, position: 0 }, // Weather emoji (☀️, 🌧️, etc.)
|
||||||
{ id: 'energy', name: 'Energy', value: 100 },
|
weatherDesc: { enabled: true, position: 1 }, // Weather description text
|
||||||
{ id: 'hygiene', name: 'Hygiene', value: 100 },
|
clock: { enabled: true, position: 2 }, // Current time display
|
||||||
{ id: 'arousal', name: 'Arousal', value: 0 }
|
date: { enabled: true, position: 3 }, // Date display
|
||||||
],
|
location: { enabled: true, position: 4 }, // Location name
|
||||||
status: {
|
stats: { enabled: true, position: 5 }, // All stats as compact numbers
|
||||||
mood: '😐',
|
attributes: { enabled: true, position: 6 } // Compact RPG attributes display
|
||||||
conditions: 'None'
|
},
|
||||||
},
|
// Desktop strip widget display options (shown in collapsed panel strip)
|
||||||
|
desktopStripWidgets: {
|
||||||
|
enabled: true, // Master toggle for strip widgets (enabled by default)
|
||||||
|
weatherIcon: { enabled: true }, // Weather emoji (☀️, 🌧️, etc.)
|
||||||
|
clock: { enabled: true }, // Current time display
|
||||||
|
date: { enabled: true }, // Date display
|
||||||
|
location: { enabled: true }, // Location name
|
||||||
|
stats: { enabled: true }, // All stats as compact numbers
|
||||||
|
attributes: { enabled: true } // Compact RPG attributes display
|
||||||
|
},
|
||||||
|
userStats: {
|
||||||
|
health: 100,
|
||||||
|
satiety: 100,
|
||||||
|
energy: 100,
|
||||||
|
hygiene: 100,
|
||||||
|
arousal: 0,
|
||||||
|
mood: '😐',
|
||||||
|
conditions: 'None',
|
||||||
|
skills: [],
|
||||||
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',
|
||||||
@@ -89,47 +160,55 @@ export let extensionSettings = {
|
|||||||
// Tracker customization configuration
|
// Tracker customization configuration
|
||||||
trackerConfig: {
|
trackerConfig: {
|
||||||
userStats: {
|
userStats: {
|
||||||
|
// Stats display mode: 'percentage' or 'number'
|
||||||
|
statsDisplayMode: 'percentage',
|
||||||
// Array of custom stats (allows add/remove/rename)
|
// Array of custom stats (allows add/remove/rename)
|
||||||
customStats: [
|
customStats: [
|
||||||
{ id: 'health', name: 'Health', enabled: true },
|
{ id: 'health', name: 'Health', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||||
{ id: 'satiety', name: 'Satiety', enabled: true },
|
{ id: 'satiety', name: 'Satiety', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||||
{ id: 'energy', name: 'Energy', enabled: true },
|
{ id: 'energy', name: 'Energy', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||||
{ id: 'hygiene', name: 'Hygiene', enabled: true },
|
{ id: 'hygiene', name: 'Hygiene', enabled: true, persistInHistory: false, maxValue: 100 },
|
||||||
{ id: 'arousal', name: 'Arousal', enabled: true }
|
{ id: 'arousal', name: 'Arousal', enabled: true, persistInHistory: false, maxValue: 100 }
|
||||||
],
|
],
|
||||||
// RPG Attributes (customizable D&D-style attributes)
|
// RPG Attributes (customizable D&D-style attributes)
|
||||||
showRPGAttributes: true,
|
showRPGAttributes: true,
|
||||||
showLevel: true, // Show/hide level in UI and prompts
|
showLevel: true, // Show/hide level in UI and prompts
|
||||||
alwaysSendAttributes: false, // If true, always send attributes; if false, only send with dice rolls
|
alwaysSendAttributes: false, // If true, always send attributes; if false, only send with dice rolls
|
||||||
rpgAttributes: [
|
rpgAttributes: [
|
||||||
{ id: 'str', name: 'STR', enabled: true },
|
{ id: 'str', name: 'STR', enabled: true, persistInHistory: false },
|
||||||
{ id: 'dex', name: 'DEX', enabled: true },
|
{ id: 'dex', name: 'DEX', enabled: true, persistInHistory: false },
|
||||||
{ id: 'con', name: 'CON', enabled: true },
|
{ id: 'con', name: 'CON', enabled: true, persistInHistory: false },
|
||||||
{ id: 'int', name: 'INT', enabled: true },
|
{ id: 'int', name: 'INT', enabled: true, persistInHistory: false },
|
||||||
{ id: 'wis', name: 'WIS', enabled: true },
|
{ id: 'wis', name: 'WIS', enabled: true, persistInHistory: false },
|
||||||
{ id: 'cha', name: 'CHA', enabled: true }
|
{ id: 'cha', name: 'CHA', enabled: true, persistInHistory: false }
|
||||||
],
|
],
|
||||||
// Status section config
|
// Status section config
|
||||||
statusSection: {
|
statusSection: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
showMoodEmoji: true,
|
showMoodEmoji: true,
|
||||||
customFields: ['Conditions'] // User can edit what to track
|
customFields: ['Conditions'], // User can edit what to track
|
||||||
|
persistInHistory: false // Persist status in historical messages
|
||||||
},
|
},
|
||||||
// Optional skills field
|
// Optional skills field
|
||||||
skillsSection: {
|
skillsSection: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
label: 'Skills', // User-editable
|
label: 'Skills', // User-editable
|
||||||
customFields: [] // Array of skill names
|
customFields: [], // Array of skill names
|
||||||
}
|
persistInHistory: false // Persist skills in historical messages
|
||||||
|
},
|
||||||
|
// Inventory persistence
|
||||||
|
inventoryPersistInHistory: false, // Persist inventory in historical messages
|
||||||
|
// Quests persistence
|
||||||
|
questsPersistInHistory: false // Persist quests in historical messages
|
||||||
},
|
},
|
||||||
infoBox: {
|
infoBox: {
|
||||||
widgets: {
|
widgets: {
|
||||||
date: { enabled: true, format: 'Weekday, Month, Year' }, // Format options in UI
|
date: { enabled: true, format: 'Weekday, Month, Year', persistInHistory: true }, // Date enabled by default for history
|
||||||
weather: { enabled: true },
|
weather: { enabled: true, persistInHistory: true }, // Weather enabled by default for history
|
||||||
temperature: { enabled: true, unit: 'C' }, // 'C' or 'F'
|
temperature: { enabled: true, unit: 'C', persistInHistory: false }, // 'C' or 'F'
|
||||||
time: { enabled: true },
|
time: { enabled: true, persistInHistory: true }, // Time enabled by default for history
|
||||||
location: { enabled: true },
|
location: { enabled: true, persistInHistory: true }, // Location enabled by default for history
|
||||||
recentEvents: { enabled: true }
|
recentEvents: { enabled: true, persistInHistory: false }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
presentCharacters: {
|
presentCharacters: {
|
||||||
@@ -159,14 +238,15 @@ export let extensionSettings = {
|
|||||||
},
|
},
|
||||||
// Custom fields (appearance, demeanor, etc. - shown after relationship, separated by |)
|
// Custom fields (appearance, demeanor, etc. - shown after relationship, separated by |)
|
||||||
customFields: [
|
customFields: [
|
||||||
{ id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)' },
|
{ id: 'appearance', name: 'Appearance', enabled: true, description: 'Visible physical appearance (clothing, hair, notable features)', persistInHistory: false },
|
||||||
{ id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state' }
|
{ id: 'demeanor', name: 'Demeanor', enabled: true, description: 'Observable demeanor or emotional state', persistInHistory: false }
|
||||||
],
|
],
|
||||||
// Thoughts configuration (separate line)
|
// Thoughts configuration (separate line)
|
||||||
thoughts: {
|
thoughts: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
name: 'Thoughts',
|
name: 'Thoughts',
|
||||||
description: 'Internal monologue (in first person POV, up to three sentences long)'
|
description: 'Internal Monologue (in first person from character\'s POV, up to three sentences long)',
|
||||||
|
persistInHistory: false
|
||||||
},
|
},
|
||||||
// Character stats toggle (optional feature)
|
// Character stats toggle (optional feature)
|
||||||
characterStats: {
|
characterStats: {
|
||||||
@@ -250,6 +330,18 @@ export let extensionSettings = {
|
|||||||
recentEvents: false // Boolean for recent events widget lock
|
recentEvents: false // Boolean for recent events widget lock
|
||||||
},
|
},
|
||||||
characters: {} // Object mapping character names to their locked fields (e.g., {"Sarah": {relationship: true, thoughts: false}})
|
characters: {} // Object mapping character names to their locked fields (e.g., {"Sarah": {relationship: true, thoughts: false}})
|
||||||
|
},
|
||||||
|
// Preset management for tracker configurations
|
||||||
|
presetManager: {
|
||||||
|
// Map of preset ID to preset data (contains name and trackerConfig)
|
||||||
|
presets: {},
|
||||||
|
// Map of character/group entity to preset ID (e.g., "char_0": "preset_123", "group_abc": "preset_456")
|
||||||
|
// Note: This is stored separately and NOT exported with presets
|
||||||
|
characterAssociations: {},
|
||||||
|
// Currently active preset ID
|
||||||
|
activePresetId: null,
|
||||||
|
// Default preset ID (used when no character association exists)
|
||||||
|
defaultPresetId: null
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -292,6 +384,24 @@ export function clearSessionAvatarPrompts() {
|
|||||||
sessionAvatarPrompts = {};
|
sessionAvatarPrompts = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-chat storage for thought-based Character Expressions portraits.
|
||||||
|
* Maps normalized character names to the current below-chat portrait URL.
|
||||||
|
*/
|
||||||
|
export let thoughtBasedExpressionPortraits = {};
|
||||||
|
|
||||||
|
export function setThoughtBasedExpressionPortraits(portraits) {
|
||||||
|
thoughtBasedExpressionPortraits = portraits && typeof portraits === 'object' ? { ...portraits } : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThoughtBasedExpressionPortrait(characterName) {
|
||||||
|
return thoughtBasedExpressionPortraits[characterName] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearThoughtBasedExpressionPortraits() {
|
||||||
|
thoughtBasedExpressionPortraits = {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks whether the last action was a swipe (for separate mode)
|
* Tracks whether the last action was a swipe (for separate mode)
|
||||||
* Used to determine whether to commit lastGeneratedData to committedTrackerData
|
* Used to determine whether to commit lastGeneratedData to committedTrackerData
|
||||||
@@ -308,6 +418,38 @@ export let isGenerating = false;
|
|||||||
*/
|
*/
|
||||||
export let isPlotProgression = false;
|
export let isPlotProgression = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag indicating if we're actively expecting a new message from generation
|
||||||
|
* (as opposed to loading chat history)
|
||||||
|
*/
|
||||||
|
export let isAwaitingNewMessage = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monotonically-increasing counter used to detect stale separate-mode tracker
|
||||||
|
* generation results. Incremented each time a new automated generation is
|
||||||
|
* triggered or a message deletion occurs so any in-flight (or pending) call
|
||||||
|
* from a previous generation can recognise that its result is no longer valid.
|
||||||
|
*/
|
||||||
|
let separateGenerationId = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current separate generation ID.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function getSeparateGenerationId() {
|
||||||
|
return separateGenerationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments and returns the new separate generation ID.
|
||||||
|
* Call this when starting a new generation or when a deletion
|
||||||
|
* invalidates any pending/in-flight generation.
|
||||||
|
* @returns {number} The new ID
|
||||||
|
*/
|
||||||
|
export function incrementSeparateGenerationId() {
|
||||||
|
return ++separateGenerationId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Temporary storage for pending dice roll (not saved until user clicks "Save Roll")
|
* Temporary storage for pending dice roll (not saved until user clicks "Save Roll")
|
||||||
*/
|
*/
|
||||||
@@ -355,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
|
||||||
@@ -408,6 +551,10 @@ export function setIsPlotProgression(value) {
|
|||||||
isPlotProgression = value;
|
isPlotProgression = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setIsAwaitingNewMessage(value) {
|
||||||
|
isAwaitingNewMessage = value;
|
||||||
|
}
|
||||||
|
|
||||||
export function setPendingDiceRoll(roll) {
|
export function setPendingDiceRoll(roll) {
|
||||||
pendingDiceRoll = roll;
|
pendingDiceRoll = roll;
|
||||||
}
|
}
|
||||||
@@ -443,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;
|
||||||
|
}
|
||||||
|
|||||||
+286
-10
@@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"settings.language.label": "Language",
|
"settings.language.label": "Language",
|
||||||
"settings.language.option.en": "English",
|
"settings.language.option.en": "English",
|
||||||
|
"settings.language.option.zh-cn": "简体中文",
|
||||||
"settings.language.option.zh-tw": "繁體中文",
|
"settings.language.option.zh-tw": "繁體中文",
|
||||||
|
"settings.language.option.ru": "Русский",
|
||||||
|
"settings.language.option.fr": "Français",
|
||||||
"settings.extensionEnabled": "Enable RPG Companion",
|
"settings.extensionEnabled": "Enable RPG Companion",
|
||||||
"settings.note": "Toggle to enable/disable the RPG Companion extension. Configure additional settings within the panel itself.",
|
"settings.note": "Toggle to enable/disable the RPG Companion extension. Configure additional settings within the panel itself.",
|
||||||
"template.settingsTitle": "RPG Companion Settings",
|
"template.settingsTitle": "RPG Companion Settings",
|
||||||
@@ -26,15 +29,19 @@
|
|||||||
"template.settingsModal.display.panelPositionOptions.right": "Right Sidebar",
|
"template.settingsModal.display.panelPositionOptions.right": "Right Sidebar",
|
||||||
"template.settingsModal.display.panelPositionOptions.left": "Left Sidebar",
|
"template.settingsModal.display.panelPositionOptions.left": "Left Sidebar",
|
||||||
"template.settingsModal.display.toggleAutoUpdate": "Auto-update after messages",
|
"template.settingsModal.display.toggleAutoUpdate": "Auto-update after messages",
|
||||||
"template.settingsModal.display.showUserStats": "Show User Stats",
|
"template.settingsModal.display.toggleAutoUpdateNote": "Automatically refresh RPG info after each message.",
|
||||||
"template.settingsModal.display.showUserStats": "Show User Stats",
|
"template.settingsModal.display.showUserStats": "Show User Stats",
|
||||||
"template.settingsModal.display.showUserStatsNote": "Enable User Stats that track your persona's statistics, mood, attributes, skills, etc.",
|
"template.settingsModal.display.showUserStatsNote": "Enable User Stats that track your persona's statistics, mood, attributes, skills, etc.",
|
||||||
"template.settingsModal.display.showInfoBox": "Show Info Box",
|
"template.settingsModal.display.showInfoBox": "Show Info Box",
|
||||||
"template.settingsModal.display.showInfoBoxNote": "Display location, time, weather, and recent events.",
|
"template.settingsModal.display.showInfoBoxNote": "Display location, time, weather, and recent events.",
|
||||||
"template.settingsModal.display.showPresentCharacters": "Show Present Characters",
|
"template.settingsModal.display.showPresentCharacters": "Show Present Characters",
|
||||||
"template.settingsModal.display.showPresentCharactersNote": "Display character portraits with their current thoughts and status.",
|
"template.settingsModal.display.showPresentCharactersNote": "Display character portraits with their current thoughts and status.",
|
||||||
"template.settingsModal.display.toggleAutoUpdate": "Auto-update after messages",
|
"template.settingsModal.display.showBelowChatPresentCharacters": "Show Below-Chat Present Characters",
|
||||||
"template.settingsModal.display.toggleAutoUpdateNote": "Automatically refresh RPG info after each message.",
|
"template.settingsModal.display.showBelowChatPresentCharactersNote": "Display a compact Present Characters panel below the chat.",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressions": "Thought-Based Expressions",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressionsNote": "Use SillyTavern Character Expressions to classify each present character's thoughts for the below-chat panel. May increase token usage depending on the selected Classifier API.",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplay": "Hide Default Expression Display",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "Hide SillyTavern's built-in Character Expressions display.",
|
||||||
"template.settingsModal.display.narratorMode": "Narrator Mode",
|
"template.settingsModal.display.narratorMode": "Narrator Mode",
|
||||||
"template.settingsModal.display.narratorModeNote": "Use character card as narrator. Infer characters from context instead of using fixed character references.",
|
"template.settingsModal.display.narratorModeNote": "Use character card as narrator. Infer characters from context instead of using fixed character references.",
|
||||||
"template.settingsModal.display.showInventory": "Show Inventory",
|
"template.settingsModal.display.showInventory": "Show Inventory",
|
||||||
@@ -45,6 +52,8 @@
|
|||||||
"template.settingsModal.display.showLockIconsNote": "Display lock/unlock icons on tracker items to prevent AI from modifying them.",
|
"template.settingsModal.display.showLockIconsNote": "Display lock/unlock icons on tracker items to prevent AI from modifying them.",
|
||||||
"template.settingsModal.display.showThoughtsInChat": "Show Thoughts",
|
"template.settingsModal.display.showThoughtsInChat": "Show Thoughts",
|
||||||
"template.settingsModal.display.showThoughtsInChatNote": "Display character thoughts as overlay bubbles next to their messages.",
|
"template.settingsModal.display.showThoughtsInChatNote": "Display character thoughts as overlay bubbles next to their messages.",
|
||||||
|
"template.settingsModal.display.showInlineThoughts": "Show Thoughts Below Message Text",
|
||||||
|
"template.settingsModal.display.showInlineThoughtsNote": "Switch between the default corner thought bubbles and thought cards below the message text.",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubble": "Always Show Thought Bubble",
|
"template.settingsModal.display.alwaysShowThoughtBubble": "Always Show Thought Bubble",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Auto-expand thought bubble without clicking the icon first",
|
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Auto-expand thought bubble without clicking the icon first",
|
||||||
"template.settingsModal.display.enableAnimations": "Enable Animations",
|
"template.settingsModal.display.enableAnimations": "Enable Animations",
|
||||||
@@ -53,6 +62,10 @@
|
|||||||
"template.settingsModal.display.showImmersiveHtmlToggleNote": "Display a toggle button to enable/disable HTML formatting in messages.",
|
"template.settingsModal.display.showImmersiveHtmlToggleNote": "Display a toggle button to enable/disable HTML formatting in messages.",
|
||||||
"template.settingsModal.display.showDialogueColoringToggle": "Show Colored Dialogues",
|
"template.settingsModal.display.showDialogueColoringToggle": "Show Colored Dialogues",
|
||||||
"template.settingsModal.display.showDialogueColoringToggleNote": "Display a toggle button to enable/disable colored dialogue formatting.",
|
"template.settingsModal.display.showDialogueColoringToggleNote": "Display a toggle button to enable/disable colored dialogue formatting.",
|
||||||
|
"template.settingsModal.display.showDeceptionToggle": "Show Deception System",
|
||||||
|
"template.settingsModal.display.showDeceptionToggleNote": "Display a toggle button to enable/disable the Deception System for marking lies and deceptions.",
|
||||||
|
"template.settingsModal.display.showOmniscienceToggle": "Show Omniscience Filter",
|
||||||
|
"template.settingsModal.display.showOmniscienceToggleNote": "Display a toggle button to enable/disable the Omniscience Filter for filtering hidden events.",
|
||||||
"template.settingsModal.display.showSpotifyMusicToggle": "Show Spotify Music",
|
"template.settingsModal.display.showSpotifyMusicToggle": "Show Spotify Music",
|
||||||
"template.settingsModal.display.showSpotifyMusicToggleNote": "Display Spotify music player with AI-suggested scene-appropriate tracks.",
|
"template.settingsModal.display.showSpotifyMusicToggleNote": "Display Spotify music player with AI-suggested scene-appropriate tracks.",
|
||||||
"template.settingsModal.display.showSnowflakesToggle": "Show Snowflakes Effect",
|
"template.settingsModal.display.showSnowflakesToggle": "Show Snowflakes Effect",
|
||||||
@@ -70,13 +83,19 @@
|
|||||||
"template.settingsModal.display.showStartEncounterNote": "Display button to initiate interactive combat encounters.",
|
"template.settingsModal.display.showStartEncounterNote": "Display button to initiate interactive combat encounters.",
|
||||||
"template.settingsModal.display.showDiceDisplay": "Show Dice Roll Display",
|
"template.settingsModal.display.showDiceDisplay": "Show Dice Roll Display",
|
||||||
"template.settingsModal.display.showDiceDisplayNote": "Display the \"Last Roll\" indicator in the panel.",
|
"template.settingsModal.display.showDiceDisplayNote": "Display the \"Last Roll\" indicator in the panel.",
|
||||||
|
"template.settingsModal.display.showCYOAToggle": "Show CYOA",
|
||||||
|
"template.settingsModal.display.showCYOAToggleNote": "Display a toggle button to enable/disable \"Choose Your Own Adventure\" formatting instruction that makes the model produce five possible actions/dialogues for you to choose from at the end of the output.",
|
||||||
|
"template.settingsModal.display.weatherPosition.background": "Show in Background",
|
||||||
|
"template.settingsModal.display.weatherPosition.backgroundNote": "Display weather effects behind the chat (standard behavior).",
|
||||||
|
"template.settingsModal.display.weatherPosition.foreground": "Show in Foreground",
|
||||||
|
"template.settingsModal.display.weatherPosition.foregroundNote": "Display weather effects in front of the chat (experimental).",
|
||||||
"template.mainPanel.autoAvatars": "Auto Avatars",
|
"template.mainPanel.autoAvatars": "Auto Avatars",
|
||||||
"template.settingsModal.advancedTitle": "Advanced",
|
"template.settingsModal.advancedTitle": "Advanced",
|
||||||
"template.settingsModal.advanced.encounterHistoryDepth": "Chat History Depth For Encounters:",
|
"template.settingsModal.advanced.encounterHistoryDepth": "Chat History Depth For Encounters:",
|
||||||
"template.settingsModal.advanced.encounterHistoryDepthNote": "Number of recent messages to include in combat initialization.",
|
"template.settingsModal.advanced.encounterHistoryDepthNote": "Number of recent messages to include in combat initialization.",
|
||||||
"template.settingsModal.advanced.autoSaveCombatLogs": "Auto-save Combat Logs",
|
"template.settingsModal.advanced.autoSaveCombatLogs": "Auto-save Combat Logs",
|
||||||
"template.settingsModal.advanced.autoSaveCombatLogsNote": "Save detailed combat logs to file for future reference and analysis.",
|
"template.settingsModal.advanced.autoSaveCombatLogsNote": "Save detailed combat logs to file for future reference and analysis.",
|
||||||
"template.settingsModal.advanced.clearCacheNote": "Clears all cached data including tracker history and temporary files.",
|
"template.settingsModal.advanced.clearCacheNote": "Clears committed and displayed tracker data for your currently active chat.",
|
||||||
"template.settingsModal.advanced.generationMode": "Generation Mode:",
|
"template.settingsModal.advanced.generationMode": "Generation Mode:",
|
||||||
"template.settingsModal.advanced.generationModeOptions.together": "Together with Main Generation",
|
"template.settingsModal.advanced.generationModeOptions.together": "Together with Main Generation",
|
||||||
"template.settingsModal.advanced.generationModeOptions.separate": "Separate Generation",
|
"template.settingsModal.advanced.generationModeOptions.separate": "Separate Generation",
|
||||||
@@ -156,13 +175,16 @@
|
|||||||
"template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "AI Instruction:",
|
"template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "AI Instruction:",
|
||||||
"template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Character Stats",
|
"template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Character Stats",
|
||||||
"template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "Track Character Stats",
|
"template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "Track Character Stats",
|
||||||
"template.trackerEditorModal.presentCharactersTab.characterStatsHint": "Create stats to track for each character (displayed as colored bars).",
|
"template.trackerEditorModal.presentCharactersTab.characterStatsHint": "Create stats to track for each character (displayed as colored numbers).",
|
||||||
"template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "Add Character Stat",
|
"template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "Add Character Stat",
|
||||||
"template.mainPanel.title": "RPG Companion",
|
"template.mainPanel.title": "RPG Companion",
|
||||||
"template.mainPanel.lastRoll": "Last Roll:",
|
"template.mainPanel.lastRoll": "Last Roll:",
|
||||||
"template.mainPanel.clearLastRoll": "Clear last roll",
|
"template.mainPanel.clearLastRoll": "Clear last roll",
|
||||||
"template.mainPanel.immersiveHtml": "Immersive HTML",
|
"template.mainPanel.immersiveHtml": "Immersive HTML",
|
||||||
"template.mainPanel.coloredDialogues": "Colored Dialogues",
|
"template.mainPanel.coloredDialogues": "Colored Dialogues",
|
||||||
|
"template.mainPanel.deceptionSystem": "Deception System",
|
||||||
|
"template.mainPanel.omniscienceFilter": "Omniscience Filter",
|
||||||
|
"template.mainPanel.cyoa": "CYOA",
|
||||||
"template.mainPanel.spotifyMusic": "Spotify Music",
|
"template.mainPanel.spotifyMusic": "Spotify Music",
|
||||||
"template.mainPanel.snowflakesEffect": "Snowflakes Effect",
|
"template.mainPanel.snowflakesEffect": "Snowflakes Effect",
|
||||||
"template.mainPanel.dynamicWeatherEffects": "Dynamic Weather",
|
"template.mainPanel.dynamicWeatherEffects": "Dynamic Weather",
|
||||||
@@ -177,10 +199,16 @@
|
|||||||
"global.listView": "List view",
|
"global.listView": "List view",
|
||||||
"global.gridView": "Grid view",
|
"global.gridView": "Grid view",
|
||||||
"global.save": "Save",
|
"global.save": "Save",
|
||||||
"global.status":"Status",
|
"global.status": "Status",
|
||||||
"global.inventory":"Inventory",
|
"global.inventory": "Inventory",
|
||||||
"global.quests":"Quests",
|
"global.quests": "Quests",
|
||||||
"global.info":"Info",
|
"global.info": "Info",
|
||||||
|
"global.removeItem": "Remove item",
|
||||||
|
"global.clickToEdit": "Click to edit",
|
||||||
|
"global.collapseExpandPanel": "Collapse/Expand Panel",
|
||||||
|
"global.refreshRpgInfo": "Refresh RPG Info",
|
||||||
|
"global.showHideApiKey": "Show/Hide API Key",
|
||||||
|
"global.closeDialog": "Close dialog",
|
||||||
"infobox.noData.title": "No data yet",
|
"infobox.noData.title": "No data yet",
|
||||||
"infobox.noData.instruction": "Generate a new response in the roleplay or switch to \"Separate Generation\" in Settings to access and click the \"Refresh RPG Info\" button",
|
"infobox.noData.instruction": "Generate a new response in the roleplay or switch to \"Separate Generation\" in Settings to access and click the \"Refresh RPG Info\" button",
|
||||||
"infobox.recentEvents.title": "Recent Events",
|
"infobox.recentEvents.title": "Recent Events",
|
||||||
@@ -213,6 +241,13 @@
|
|||||||
"inventory.assets.addAssetButton": "Add Asset",
|
"inventory.assets.addAssetButton": "Add Asset",
|
||||||
"inventory.assets.addAssetPlaceholder": "Enter asset name...",
|
"inventory.assets.addAssetPlaceholder": "Enter asset name...",
|
||||||
"inventory.assets.description": "Assets include vehicles (cars, motorcycles), property (homes, apartments), and major equipment (workshop tools, special items).",
|
"inventory.assets.description": "Assets include vehicles (cars, motorcycles), property (homes, apartments), and major equipment (workshop tools, special items).",
|
||||||
|
"inventory.onPerson.addItemTitle": "Add new item",
|
||||||
|
"inventory.clothing.addItemTitle": "Add new clothing item",
|
||||||
|
"inventory.stored.addLocationTitle": "Add new storage location",
|
||||||
|
"inventory.stored.addItemToLocationTitle": "Add item to this location",
|
||||||
|
"inventory.stored.removeLocationTitle": "Remove this storage location",
|
||||||
|
"inventory.assets.addItemTitle": "Add new asset",
|
||||||
|
"inventory.assets.removeAssetTitle": "Remove asset",
|
||||||
"quests.section.main": "Main Quest",
|
"quests.section.main": "Main Quest",
|
||||||
"quests.section.optional": "Optional Quests",
|
"quests.section.optional": "Optional Quests",
|
||||||
"quests.main.title": "Main Quests",
|
"quests.main.title": "Main Quests",
|
||||||
@@ -225,6 +260,8 @@
|
|||||||
"quests.optional.addQuestPlaceholder": "Enter optional quest title...",
|
"quests.optional.addQuestPlaceholder": "Enter optional quest title...",
|
||||||
"quests.optional.empty": "No active optional quests",
|
"quests.optional.empty": "No active optional quests",
|
||||||
"quests.optional.hint": "Optional quests are side objectives that complement your main story.",
|
"quests.optional.hint": "Optional quests are side objectives that complement your main story.",
|
||||||
|
"quests.editQuestTitle": "Edit quest",
|
||||||
|
"quests.removeQuestTitle": "Complete/Remove quest",
|
||||||
"checkpoint.setChapterStart": "Set Chapter Start",
|
"checkpoint.setChapterStart": "Set Chapter Start",
|
||||||
"checkpoint.clearChapterStart": "Clear Chapter Start",
|
"checkpoint.clearChapterStart": "Clear Chapter Start",
|
||||||
"checkpoint.indicator": "Chapter Start",
|
"checkpoint.indicator": "Chapter Start",
|
||||||
@@ -233,5 +270,244 @@
|
|||||||
"musicPlayer.noMusic": "AI will suggest music when appropriate for the scene",
|
"musicPlayer.noMusic": "AI will suggest music when appropriate for the scene",
|
||||||
"errors.parsingError": "RPG Companion Trackers' parsing error! The model returned an incorrect format. If the issue persists, consider changing the model for generations.",
|
"errors.parsingError": "RPG Companion Trackers' parsing error! The model returned an incorrect format. If the issue persists, consider changing the model for generations.",
|
||||||
"settings.recommendedModels.title": "Recommended Models",
|
"settings.recommendedModels.title": "Recommended Models",
|
||||||
"settings.recommendedModels.description": "For the extension to work properly, **it is not recommended to use any models below 20B, especially if they're old.** It works best with the SOTA models such as Deepseek, Claude, GPT, or Gemini."
|
"settings.recommendedModels.description": "For the extension to work properly, **it is not recommended to use any models below 20B, especially if they're old.** It works best with the SOTA models such as Deepseek, Claude, GPT, or Gemini.",
|
||||||
|
"thoughts.addCharacter": "Add Character",
|
||||||
|
"thoughts.locked": "Locked",
|
||||||
|
"thoughts.unlocked": "Unlocked",
|
||||||
|
"thoughts.clickToEdit": "Click to edit",
|
||||||
|
"thoughts.clickToUpload": "Click to upload avatar",
|
||||||
|
"thoughts.removeCharacter": "Remove character",
|
||||||
|
"thoughts.empty": "No character data generated yet",
|
||||||
|
"userStats.level": "LVL",
|
||||||
|
"userStats.clickToEditLevel": "Click to edit level",
|
||||||
|
"userStats.statsLocked": "Locked - AI cannot change stats",
|
||||||
|
"userStats.statsUnlocked": "Unlocked - AI can change stats",
|
||||||
|
"userStats.clickToEditStatName": "Click to edit stat name",
|
||||||
|
"userStats.clickToEditStatValue": "Click to edit",
|
||||||
|
"userStats.moodLocked": "Locked - AI cannot change mood",
|
||||||
|
"userStats.moodUnlocked": "Unlocked - AI can change mood",
|
||||||
|
"userStats.clickToEditEmoji": "Click to edit emoji",
|
||||||
|
"userStats.skillsLocked": "Locked - AI cannot change skills",
|
||||||
|
"userStats.skillsUnlocked": "Unlocked - AI can change skills",
|
||||||
|
"userStats.clickToEditSkills": "Click to edit skills",
|
||||||
|
"userStats.empty": "No statuses generated yet",
|
||||||
|
"infoBox.clickToEdit": "Click to edit",
|
||||||
|
"infoBox.locked": "Locked - AI cannot change this",
|
||||||
|
"infoBox.unlocked": "Unlocked - AI can change this",
|
||||||
|
"infoBox.weatherFallback": "Weather",
|
||||||
|
"infoBox.locationFallback": "Location",
|
||||||
|
"stats.health": "Health",
|
||||||
|
"stats.satiety": "Satiety",
|
||||||
|
"stats.energy": "Energy",
|
||||||
|
"stats.hygiene": "Hygiene",
|
||||||
|
"stats.arousal": "Arousal",
|
||||||
|
"stats.str": "STR",
|
||||||
|
"stats.dex": "DEX",
|
||||||
|
"stats.con": "CON",
|
||||||
|
"stats.int": "INT",
|
||||||
|
"stats.wis": "WIS",
|
||||||
|
"stats.cha": "CHA",
|
||||||
|
"stats.displayMode": "Display Mode:",
|
||||||
|
"stats.displayMode.percentage": "Percentage",
|
||||||
|
"stats.displayMode.number": "Number",
|
||||||
|
"dice.title": "Roll Dice",
|
||||||
|
"dice.numberOfDice": "Number of Dice:",
|
||||||
|
"dice.diceType": "Dice Type:",
|
||||||
|
"dice.rolling": "Rolling...",
|
||||||
|
"dice.result": "Result:",
|
||||||
|
"dice.saveRoll": "Save Roll",
|
||||||
|
"preset.createNewPresetTitle": "Create New Preset",
|
||||||
|
"preset.deleteCurrentPresetTitle": "Delete Current Preset",
|
||||||
|
"preset.setDefaultPresetTitle": "Set as Default Preset",
|
||||||
|
"preset.defaultPresetDescription": "This is the default preset",
|
||||||
|
"preset.label": "Preset:",
|
||||||
|
"preset.useThisPresetFor": "Use this preset for: ",
|
||||||
|
"stats.showLevel": "Show Level",
|
||||||
|
"dateFormat.weekdayMonthYear": "Weekday, Month, Year",
|
||||||
|
"dateFormat.dayNumericalMonthYear": "Day (Numerical), Month, Year",
|
||||||
|
"historyPersistence.tabTitle": "History Persistence",
|
||||||
|
"historyPersistence.settingsTitle": "History Persistence Settings",
|
||||||
|
"historyPersistence.enable": "Enable History Persistence",
|
||||||
|
"template.trackerEditorModal.tabs.historyPersistence": "History Persistence",
|
||||||
|
"historyPersistence.hint": "Inject selected tracker data into historical messages to help the AI maintain continuity for time-sensitive events, weather changes, and location tracking.",
|
||||||
|
"historyPersistence.sendAllEnabledStats": "Send All Enabled Stats on Refresh",
|
||||||
|
"historyPersistence.sendAllEnabledStatsHint": "When enabled, Refresh RPG Info will include all enabled stats from the preset in history context, ignoring the individual selections below.",
|
||||||
|
"historyPersistence.numberOfMessages": "Number of messages to include (0 = all available):",
|
||||||
|
"historyPersistence.injectionPosition": "Injection Position:",
|
||||||
|
"historyPersistence.injectionPosition.userMessageEnd": "End of the User's Message",
|
||||||
|
"historyPersistence.injectionPosition.assistantMessageEnd": "End of the Assistant's Message",
|
||||||
|
"historyPersistence.customContextPreamble": "Custom Context Preamble:",
|
||||||
|
"historyPersistence.customContextPreamblePlaceholder": "Context for that moment:",
|
||||||
|
"historyPersistence.userStatsSection": "User Stats",
|
||||||
|
"historyPersistence.userStatsHint": "Select which stats should be included in historical messages.",
|
||||||
|
"historyPersistence.statusSection": "Status (Mood/Conditions)",
|
||||||
|
"historyPersistence.inventory": "Inventory",
|
||||||
|
"historyPersistence.quests": "Quests",
|
||||||
|
"historyPersistence.infoBoxSection": "Info Box",
|
||||||
|
"historyPersistence.infoBoxHint": "Select which info box fields should be included in historical messages. These are recommended for time tracking.",
|
||||||
|
"historyPersistence.presentCharactersSection": "Present Characters",
|
||||||
|
"historyPersistence.presentCharactersHint": "Select which character fields should be included in historical messages.",
|
||||||
|
"historyPersistence.widget.date": "Date",
|
||||||
|
"historyPersistence.widget.weather": "Weather",
|
||||||
|
"historyPersistence.widget.temperature": "Temperature",
|
||||||
|
"historyPersistence.widget.time": "Time",
|
||||||
|
"historyPersistence.widget.location": "Location",
|
||||||
|
"historyPersistence.widget.recentEvents": "Recent Events",
|
||||||
|
"historyPersistence.thoughts": "Thoughts",
|
||||||
|
"historyPersistence.skills": "Skills",
|
||||||
|
"template.promptsEditor.button": "Customize Prompts",
|
||||||
|
"template.promptsEditor.buttonNote": "Edit all AI prompts used for generation, plot progression, and combat encounters.",
|
||||||
|
"template.promptsEditor.title": "Customize Prompts",
|
||||||
|
"template.promptsEditor.description": "Customize the AI prompts used throughout the extension. Leave fields empty to use defaults.",
|
||||||
|
"template.promptsEditor.restoreDefault": "Restore Default",
|
||||||
|
"template.promptsEditor.htmlPrompt.title": "HTML Prompt",
|
||||||
|
"template.promptsEditor.htmlPrompt.note": "Injected when \"Enable Immersive HTML\" is enabled. Affects all generation modes.",
|
||||||
|
"template.promptsEditor.dialogueColoringPrompt.title": "Dialogue Coloring Prompt",
|
||||||
|
"template.promptsEditor.dialogueColoringPrompt.note": "Injected when \"Enable Colored Dialogues\" is enabled. Affects all generation modes.",
|
||||||
|
"template.promptsEditor.deceptionPrompt.title": "Deception System Prompt",
|
||||||
|
"template.promptsEditor.deceptionPrompt.note": "Injected when \"Enable Deception System\" is enabled. Instructs AI to mark lies and deceptions with hidden tags.",
|
||||||
|
"template.promptsEditor.omnisciencePrompt.title": "Omniscience Filter Prompt",
|
||||||
|
"template.promptsEditor.omnisciencePrompt.note": "Injected when \"Enable Omniscience Filter\" is enabled. Instructs AI to separate information the player character cannot perceive into hidden <ofilter> tags.",
|
||||||
|
"template.promptsEditor.cyoaPrompt.title": "CYOA Prompt",
|
||||||
|
"template.promptsEditor.cyoaPrompt.note": "Injected when \"Enable CYOA\" is enabled. Instructs AI to end responses with numbered action choices. Uses very high priority (depth 102) to ensure it's the last instruction.",
|
||||||
|
"template.promptsEditor.spotifyPrompt.title": "Spotify Music Prompt",
|
||||||
|
"template.promptsEditor.spotifyPrompt.note": "Injected when \"Enable Spotify Music\" is enabled. Asks AI to suggest appropriate music for the scene.",
|
||||||
|
"template.promptsEditor.narratorPrompt.title": "Narrator Mode Prompt",
|
||||||
|
"template.promptsEditor.narratorPrompt.note": "Injected when \"Narrator Mode\" is enabled. Instructs AI to infer characters from context.",
|
||||||
|
"template.promptsEditor.contextPrompt.title": "Context Instructions Prompt",
|
||||||
|
"template.promptsEditor.contextPrompt.note": "Injected in Separate/External mode after the context summary. Tells the AI how to use the context.",
|
||||||
|
"template.promptsEditor.randomPlotPrompt.title": "Random Plot Progression Prompt",
|
||||||
|
"template.promptsEditor.randomPlotPrompt.note": "Injected when the \"Randomized Plot\" button is clicked. Introduces random elements to the story.",
|
||||||
|
"template.promptsEditor.naturalPlotPrompt.title": "Natural Plot Progression Prompt",
|
||||||
|
"template.promptsEditor.naturalPlotPrompt.note": "Injected when the \"Natural Plot\" button is clicked. Progresses the story naturally.",
|
||||||
|
"template.promptsEditor.avatarPrompt.title": "Avatar Generation Instruction",
|
||||||
|
"template.promptsEditor.avatarPrompt.note": "Instructions for LLM when generating avatar image prompts. Used by Auto-generate Missing Avatars feature.",
|
||||||
|
"template.promptsEditor.trackerPrompt.title": "Tracker Instructions",
|
||||||
|
"template.promptsEditor.trackerPrompt.note": "Instruction portion only (format specification is hardcoded). {userName} will be replaced with the user's name.",
|
||||||
|
"template.promptsEditor.trackerContinuationPrompt.title": "Tracker Continuation Instruction",
|
||||||
|
"template.promptsEditor.trackerContinuationPrompt.note": "Instructions added after tracker format specifications, telling the AI how to continue the narrative.",
|
||||||
|
"template.promptsEditor.combatPrompt.title": "Combat Narrative Style Instruction",
|
||||||
|
"template.promptsEditor.combatPrompt.note": "Writing style instructions for combat encounters. Includes prose quality guidelines and anti-repetition rules. {userName} will be replaced with the user's name.",
|
||||||
|
"template.settingsModal.mobileFabTitle": "Mobile Button Widgets",
|
||||||
|
"template.settingsModal.mobileFabNote": "Show compact info widgets around the floating button on mobile. Widgets are positioned automatically.",
|
||||||
|
"template.settingsModal.mobileFab.enabled": "Enable Floating Mobile Widgets",
|
||||||
|
"template.settingsModal.mobileFab.enabledNote": "Master toggle to show info widgets around the mobile floating button.",
|
||||||
|
"template.settingsModal.mobileFab.weatherIcon": "Weather Icon",
|
||||||
|
"template.settingsModal.mobileFab.weatherDesc": "Weather Description",
|
||||||
|
"template.settingsModal.mobileFab.clock": "Time/Clock",
|
||||||
|
"template.settingsModal.mobileFab.date": "Date",
|
||||||
|
"template.settingsModal.mobileFab.location": "Location",
|
||||||
|
"template.settingsModal.mobileFab.stats": "Stats (Health, Energy, etc.)",
|
||||||
|
"template.settingsModal.mobileFab.attributes": "RPG Attributes (STR, DEX, etc.)",
|
||||||
|
"template.settingsModal.desktopStripTitle": "Desktop Collapsed Strip Widgets",
|
||||||
|
"template.settingsModal.desktopStripNote": "Show compact info widgets in the collapsed panel strip on desktop. Displays stats vertically without needing to expand the panel.",
|
||||||
|
"template.settingsModal.desktopStrip.enabled": "Enable Strip Widgets",
|
||||||
|
"template.settingsModal.desktopStrip.enabledNote": "Shows widgets in the collapsed panel strip for quick access to stats.",
|
||||||
|
"template.settingsModal.desktopStrip.weatherIcon": "Weather Icon",
|
||||||
|
"template.settingsModal.desktopStrip.clock": "Time/Clock",
|
||||||
|
"template.settingsModal.desktopStrip.date": "Date",
|
||||||
|
"template.settingsModal.desktopStrip.location": "Location",
|
||||||
|
"template.settingsModal.desktopStrip.stats": "Stats (Health, Energy, etc.)",
|
||||||
|
"template.settingsModal.desktopStrip.attributes": "RPG Attributes (STR, DEX, etc.)",
|
||||||
|
"plotProgression.buttons.randomizedPlot": "Randomized Plot",
|
||||||
|
"plotProgression.buttons.naturalPlot": "Natural Plot",
|
||||||
|
"plotProgression.buttons.enterEncounter": "Enter Encounter",
|
||||||
|
"plotProgression.tooltips.randomizedPlot": "Generate a random plot twist or event",
|
||||||
|
"plotProgression.tooltips.naturalPlot": "Continue the story naturally without twists",
|
||||||
|
"plotProgression.tooltips.enterEncounter": "Enter combat encounter",
|
||||||
|
"encounter.configModal.title": "Configure Combat Narrative",
|
||||||
|
"encounter.configModal.combatNarrativeStyle": "Combat Narrative Style",
|
||||||
|
"encounter.configModal.combatSummaryStyle": "Combat Summary Style",
|
||||||
|
"encounter.configModal.labels.tense": "Tense:",
|
||||||
|
"encounter.configModal.labels.person": "Person:",
|
||||||
|
"encounter.configModal.labels.narration": "Narration:",
|
||||||
|
"encounter.configModal.labels.pointOfView": "Point of View:",
|
||||||
|
"encounter.configModal.options.present": "Present",
|
||||||
|
"encounter.configModal.options.past": "Past",
|
||||||
|
"encounter.configModal.options.firstPerson": "First Person",
|
||||||
|
"encounter.configModal.options.secondPerson": "Second Person",
|
||||||
|
"encounter.configModal.options.thirdPerson": "Third Person",
|
||||||
|
"encounter.configModal.options.omniscient": "Omniscient",
|
||||||
|
"encounter.configModal.options.limited": "Limited",
|
||||||
|
"encounter.configModal.placeholders.narrator": "narrator",
|
||||||
|
"encounter.configModal.rememberSettings": "Remember these settings for future encounters",
|
||||||
|
"encounter.configModal.buttons.proceed": "Proceed",
|
||||||
|
"encounter.ui.concludeEncounterTitle": "Conclude encounter early",
|
||||||
|
"encounter.ui.closeTitle": "Close (ends combat)",
|
||||||
|
"encounter.ui.initializingCombat": "Initializing combat...",
|
||||||
|
"encounter.ui.combatBegins": "Combat begins!",
|
||||||
|
"encounter.ui.allEnemies": "All Enemies",
|
||||||
|
"encounter.ui.areaOfEffect": "Area of Effect",
|
||||||
|
"encounter.ui.youHaveBeenDefeated": "You have been defeated...",
|
||||||
|
"encounter.ui.attacks": "Attacks",
|
||||||
|
"encounter.ui.items": "Items",
|
||||||
|
"encounter.ui.customAction": "Custom Action",
|
||||||
|
"encounter.ui.customActionPlaceholder": "Describe what you want to do...",
|
||||||
|
"encounter.ui.generatingCombatSummary": "Generating combat summary...",
|
||||||
|
"encounter.ui.pleaseWait": "Please wait...",
|
||||||
|
"encounter.ui.failedToCreateSummary": "Failed to create summary. You can close this window.",
|
||||||
|
"encounter.ui.wrongFormatDetected": "Wrong Format Detected",
|
||||||
|
"encounter.ui.concludeEncounterButton": "Conclude Encounter",
|
||||||
|
"encounter.ui.combatEncounterTitle": "Combat Encounter",
|
||||||
|
"encounter.ui.errorGeneratingCombatSummary": "Error generating combat summary.",
|
||||||
|
"encounter.ui.closeCombatWindow": "Close Combat Window",
|
||||||
|
"encounter.ui.combatLog": "Combat Log",
|
||||||
|
"encounter.ui.selectTarget": "Select Target",
|
||||||
|
"encounter.ui.submit": "Submit",
|
||||||
|
"encounter.ui.regenerate": "Regenerate",
|
||||||
|
"encounter.ui.or": "OR",
|
||||||
|
"global.locked": "Locked",
|
||||||
|
"global.unlocked": "Unlocked",
|
||||||
|
"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.stored.removeLocationConfirm": "Remove \"{location}\"? This will delete all items stored there.",
|
||||||
|
"userStats.clickToEdit": "Click to edit",
|
||||||
|
"quests.main.addQuestTitle": "Add main quests",
|
||||||
|
"quests.optional.addQuestTitle": "Add optional quest"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
{
|
||||||
|
"settings.language.label": "Langue",
|
||||||
|
"settings.language.option.en": "English",
|
||||||
|
"settings.language.option.zh-cn": "简体中文",
|
||||||
|
"settings.language.option.zh-tw": "繁體中文",
|
||||||
|
"settings.language.option.ru": "Русский",
|
||||||
|
"settings.language.option.fr": "Français",
|
||||||
|
"settings.extensionEnabled": "Activer RPG Companion",
|
||||||
|
"settings.note": "Basculez pour activer/désactiver l'extension RPG Companion. Configurez des paramètres supplémentaires dans le panneau lui-même.",
|
||||||
|
"template.settingsTitle": "Paramètres RPG Companion",
|
||||||
|
"template.settingsModal.themeTitle": "Thème",
|
||||||
|
"template.settingsModal.themeLabel": "Thème Visuel :",
|
||||||
|
"template.settingsModal.themeOptions.default": "Défaut",
|
||||||
|
"template.settingsModal.themeOptions.sciFi": "Sci-Fi (Synthwave)",
|
||||||
|
"template.settingsModal.themeOptions.fantasy": "Fantasy (Parchemin Rustique)",
|
||||||
|
"template.settingsModal.themeOptions.cyberpunk": "Cyberpunk (Grille Néon)",
|
||||||
|
"template.settingsModal.themeOptions.custom": "Personnalisé",
|
||||||
|
"template.settingsModal.themeOptions.custom.background": "Arrière-plan :",
|
||||||
|
"template.settingsModal.themeOptions.custom.accent": "Accent :",
|
||||||
|
"template.settingsModal.themeOptions.custom.text": "Texte :",
|
||||||
|
"template.settingsModal.themeOptions.custom.highlight": "Surlignage :",
|
||||||
|
"template.settingsModal.theme.statBarLow": "Couleur Barre Stat (Bas) :",
|
||||||
|
"template.settingsModal.theme.statBarLowNote": "Couleur lorsque les stats sont à 0%.",
|
||||||
|
"template.settingsModal.theme.statBarHigh": "Couleur Barre Stat (Haut) :",
|
||||||
|
"template.settingsModal.theme.statBarHighNote": "Couleur lorsque les stats sont à 100%.",
|
||||||
|
"template.settingsModal.displayTitle": "Options d'Affichage",
|
||||||
|
"template.settingsModal.displayNote": "Vous pouvez activer/désactiver l'extension complète RPG Companion dans l'onglet Extensions de SillyTavern.",
|
||||||
|
"template.settingsModal.display.panelPosition": "Position du Panneau :",
|
||||||
|
"template.settingsModal.display.panelPositionOptions.right": "Barre Latérale Droite",
|
||||||
|
"template.settingsModal.display.panelPositionOptions.left": "Barre Latérale Gauche",
|
||||||
|
"template.settingsModal.display.toggleAutoUpdate": "Mise à jour auto après messages",
|
||||||
|
"template.settingsModal.display.toggleAutoUpdateNote": "Rafraîchir automatiquement les infos RPG après chaque message.",
|
||||||
|
"template.settingsModal.display.showUserStats": "Afficher Stats Utilisateur",
|
||||||
|
"template.settingsModal.display.showUserStatsNote": "Activer les Stats Utilisateur pour suivre les statistiques, l'humeur, les attributs, les compétences, etc. de votre persona.",
|
||||||
|
"template.settingsModal.display.showInfoBox": "Afficher Boîte Info",
|
||||||
|
"template.settingsModal.display.showInfoBoxNote": "Afficher le lieu, l'heure, la météo et les événements récents.",
|
||||||
|
"template.settingsModal.display.showPresentCharacters": "Afficher Personnages Présents",
|
||||||
|
"template.settingsModal.display.showPresentCharactersNote": "Afficher les portraits des personnages avec leurs pensées actuelles et leur statut.",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharacters": "Afficher les personnages sous le chat",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharactersNote": "Afficher un panneau compact des personnages présents sous le chat.",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressions": "Expressions basées sur les pensées",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressionsNote": "Utiliser Character Expressions de SillyTavern pour classifier les pensées de chaque personnage présent dans le panneau sous le chat. L'utilisation de tokens peut augmenter selon l'API de classification sélectionnée.",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplay": "Masquer l'affichage d'expressions par défaut",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "Masquer l'affichage intégré des expressions de personnage de SillyTavern.",
|
||||||
|
"template.settingsModal.display.narratorMode": "Mode Narrateur",
|
||||||
|
"template.settingsModal.display.narratorModeNote": "Utiliser la carte de personnage comme narrateur. Déduire les personnages du contexte au lieu d'utiliser des références de personnages fixes.",
|
||||||
|
"template.settingsModal.display.showInventory": "Afficher Inventaire",
|
||||||
|
"template.settingsModal.display.showInventoryNote": "Suivre les objets transportés, les vêtements portés, les objets stockés et les biens.",
|
||||||
|
"template.settingsModal.display.showQuests": "Afficher Quêtes",
|
||||||
|
"template.settingsModal.display.showQuestsNote": "Gérer les quêtes principales et optionnelles avec des objectifs.",
|
||||||
|
"template.settingsModal.display.showLockIcons": "Afficher Icônes Verrouillage",
|
||||||
|
"template.settingsModal.display.showLockIconsNote": "Afficher les icônes de verrouillage/déverrouillage sur les éléments de suivi pour empêcher l'IA de les modifier.",
|
||||||
|
"template.settingsModal.display.showThoughtsInChat": "Afficher Pensées",
|
||||||
|
"template.settingsModal.display.showThoughtsInChatNote": "Afficher les pensées des personnages sous forme de bulles superposées à côté de leurs messages.",
|
||||||
|
"template.settingsModal.display.showInlineThoughts": "Afficher les pensées sous le texte du message",
|
||||||
|
"template.settingsModal.display.showInlineThoughtsNote": "Basculer entre les bulles de pensée dans le coin par défaut et des cartes de pensée sous le texte du message.",
|
||||||
|
"template.settingsModal.display.alwaysShowThoughtBubble": "Toujours Afficher Bulle Pensée",
|
||||||
|
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Développer automatiquement la bulle de pensée sans cliquer sur l'icône d'abord",
|
||||||
|
"template.settingsModal.display.enableAnimations": "Activer Animations",
|
||||||
|
"template.settingsModal.display.enableAnimationsNote": "Transitions fluides pour les stats, les mises à jour de contenu et les lancers de dés.",
|
||||||
|
"template.settingsModal.display.showImmersiveHtmlToggle": "Afficher HTML Immersif",
|
||||||
|
"template.settingsModal.display.showImmersiveHtmlToggleNote": "Afficher un bouton pour activer/désactiver le formatage HTML dans les messages.",
|
||||||
|
"template.settingsModal.display.showDialogueColoringToggle": "Afficher Dialogues Colorés",
|
||||||
|
"template.settingsModal.display.showDialogueColoringToggleNote": "Afficher un bouton pour activer/désactiver le formatage des dialogues colorés.",
|
||||||
|
"template.settingsModal.display.showDeceptionToggle": "Afficher Système Déception",
|
||||||
|
"template.settingsModal.display.showDeceptionToggleNote": "Afficher un bouton pour activer/désactiver le Système de Déception pour marquer les mensonges.",
|
||||||
|
"template.settingsModal.display.showOmniscienceToggle": "Afficher Filtre Omniscience",
|
||||||
|
"template.settingsModal.display.showOmniscienceToggleNote": "Afficher un bouton pour activer/désactiver le Filtre d'Omniscience pour filtrer les événements cachés.",
|
||||||
|
"template.settingsModal.display.showSpotifyMusicToggle": "Afficher Musique Spotify",
|
||||||
|
"template.settingsModal.display.showSpotifyMusicToggleNote": "Afficher le lecteur Spotify avec des pistes suggérées par l'IA appropriées à la scène.",
|
||||||
|
"template.settingsModal.display.showSnowflakesToggle": "Afficher Effet Flocons",
|
||||||
|
"template.settingsModal.display.showDynamicWeatherToggle": "Afficher Effets Météo Dynamiques",
|
||||||
|
"template.settingsModal.display.showDynamicWeatherToggleNote": "Afficher un bouton pour activer/désactiver les effets météo animés.",
|
||||||
|
"template.settingsModal.display.showNarratorMode": "Afficher Mode Narrateur",
|
||||||
|
"template.settingsModal.display.showNarratorModeNote": "Afficher un bouton pour activer/désactiver le mode narrateur (déduire les personnages du contexte).",
|
||||||
|
"template.settingsModal.display.showAutoAvatars": "Afficher Génération Auto Avatars",
|
||||||
|
"template.settingsModal.display.showAutoAvatarsNote": "Afficher un bouton pour générer automatiquement des avatars pour les personnages sans image.",
|
||||||
|
"template.settingsModal.display.showRandomizedPlot": "Afficher Progression Intrigue Aléatoire",
|
||||||
|
"template.settingsModal.display.showRandomizedPlotNote": "Afficher un bouton pour des invites de progression d'intrigue générées aléatoirement par l'IA.",
|
||||||
|
"template.settingsModal.display.showNaturalPlot": "Afficher Progression Intrigue Naturelle",
|
||||||
|
"template.settingsModal.display.showNaturalPlotNote": "Afficher un bouton pour des invites de continuation narrative conscientes du contexte.",
|
||||||
|
"template.settingsModal.display.showStartEncounter": "Afficher Démarrer Rencontre",
|
||||||
|
"template.settingsModal.display.showStartEncounterNote": "Afficher un bouton pour initier des rencontres de combat interactives.",
|
||||||
|
"template.settingsModal.display.showDiceDisplay": "Afficher Lancer de Dés",
|
||||||
|
"template.settingsModal.display.showDiceDisplayNote": "Afficher l'indicateur \"Dernier Lancer\" dans le panneau.",
|
||||||
|
"template.mainPanel.autoAvatars": "Avatars Auto",
|
||||||
|
"template.settingsModal.advancedTitle": "Avancé",
|
||||||
|
"template.settingsModal.advanced.encounterHistoryDepth": "Profondeur Historique Rencontre :",
|
||||||
|
"template.settingsModal.advanced.encounterHistoryDepthNote": "Nombre de messages récents à inclure dans l'initialisation du combat.",
|
||||||
|
"template.settingsModal.advanced.autoSaveCombatLogs": "Sauvegarde Auto Journaux Combat",
|
||||||
|
"template.settingsModal.advanced.autoSaveCombatLogsNote": "Sauvegarder les journaux de combat détaillés dans un fichier pour référence future et analyse.",
|
||||||
|
"template.settingsModal.advanced.clearCacheNote": "Efface les données de suivi validées et affichées pour votre chat actuellement actif.",
|
||||||
|
"template.settingsModal.advanced.generationMode": "Mode de Génération :",
|
||||||
|
"template.settingsModal.advanced.generationModeOptions.together": "Ensemble avec Génération Principale",
|
||||||
|
"template.settingsModal.advanced.generationModeOptions.separate": "Génération Séparée",
|
||||||
|
"template.settingsModal.advanced.generationModeNote": "Ensemble : Ajoute le suivi RPG au jeu de rôle principal. Séparé : Génère les données RPG séparément (manuel ou auto). Externe : Se connecte directement à un point de terminaison compatible OpenAI.",
|
||||||
|
"template.settingsModal.advanced.generationModeOptions.external": "API Externe",
|
||||||
|
"template.settingsModal.advanced.externalApi.title": "Paramètres API Externe",
|
||||||
|
"template.settingsModal.advanced.externalApi.baseUrl": "URL de base API",
|
||||||
|
"template.settingsModal.advanced.externalApi.baseUrlNote": "Point de terminaison compatible OpenAI (ex: OpenAI, OpenRouter, serveur LLM local).",
|
||||||
|
"template.settingsModal.advanced.externalApi.apiKey": "Clé API",
|
||||||
|
"template.settingsModal.advanced.externalApi.apiKeyNote": "Votre clé API pour le service externe.",
|
||||||
|
"template.settingsModal.advanced.externalApi.model": "Modèle",
|
||||||
|
"template.settingsModal.advanced.externalApi.modelNote": "Identifiant du modèle (ex: gpt-4o-mini, claude-3-haiku, mistral-7b).",
|
||||||
|
"template.settingsModal.advanced.externalApi.maxTokens": "Max Tokens",
|
||||||
|
"template.settingsModal.advanced.externalApi.temperature": "Température",
|
||||||
|
"template.settingsModal.advanced.externalApi.testConnection": "Tester Connexion",
|
||||||
|
"template.settingsModal.advanced.contextMessages": "Messages de Contexte :",
|
||||||
|
"template.settingsModal.advanced.contextMessagesNote": "Nombre de messages récents à inclure.",
|
||||||
|
"template.settingsModal.advanced.useSeparatePreset": "Utiliser modèle connecté au preset RPG Companion Trackers",
|
||||||
|
"template.settingsModal.advanced.useSeparatePresetNote": "Si activé, la génération de suivi utilisera le modèle du preset \"RPG Companion Trackers\" au lieu de votre modèle API principal. Le preset sera commuté automatiquement pendant la génération et restauré après. Sélectionnez le modèle souhaité dans ce preset et assurez-vous que l'option \"Lier les presets aux connexions API\" est activée (à côté des boutons import/export preset).",
|
||||||
|
"template.settingsModal.advanced.skipInjections": "Sauter Injections Pendant Générations Guidées :",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsOptions.none": "Ne jamais sauter",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsOptions.impersonation": "Seulement sur demandes d'imitation",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsOptions.guided": "Toujours pour invites guidées ou silencieuses",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsNote": "Si défini, l'extension n'injectera pas les invites de suivi, exemples ou instructions HTML selon le mode sélectionné lorsqu'une génération guidée (via `instruct` ou `quiet_prompt`) est détectée. Utile lors de l'utilisation de GuidedGenerations ou extensions similaires.",
|
||||||
|
"template.settingsModal.advanced.customHtmlPromptTitle": "Invite HTML Personnalisée :",
|
||||||
|
"template.settingsModal.advanced.restoreDefaultHtmlPrompt": "Restaurer Défaut",
|
||||||
|
"template.settingsModal.advanced.customHtmlPromptNote": "Personnalisez l'invite HTML injectée lorsque \"Activer HTML Immersif\" est activé. L'invite par défaut est affichée ci-dessus - vous pouvez l'éditer directement ou la remplacer entièrement. Cliquez sur \"Restaurer Défaut\" pour réinitialiser. Cela affecte tous les modes de génération (ensemble, séparé et progression d'intrigue).",
|
||||||
|
"template.settingsModal.advanced.clearCache": "Effacer Cache Extension",
|
||||||
|
"template.settingsModal.advanced.resetFabPositions": "Réinitialiser Positions Boutons",
|
||||||
|
"template.settingsModal.advanced.resetFabPositionsNote": "Réinitialise tous les boutons d'action flottants (bascule, rafraîchir, debug) à leurs positions par défaut en haut à gauche. Utile si les boutons sont hors écran.",
|
||||||
|
"template.trackerEditorModal.title": "Éditer Suivis",
|
||||||
|
"template.trackerEditorModal.tabs.userStats": "Stats Utilisateur",
|
||||||
|
"template.trackerEditorModal.tabs.infoBox": "Boîte Info",
|
||||||
|
"template.trackerEditorModal.tabs.presentCharacters": "Personnages Présents",
|
||||||
|
"template.trackerEditorModal.buttons.reset": "Réinitialiser",
|
||||||
|
"template.trackerEditorModal.buttons.cancel": "Annuler",
|
||||||
|
"template.trackerEditorModal.buttons.save": "Sauvegarder & Appliquer",
|
||||||
|
"template.trackerEditorModal.buttons.export": "Exporter",
|
||||||
|
"template.trackerEditorModal.buttons.import": "Importer",
|
||||||
|
"template.trackerEditorModal.messages.exportSuccess": "Preset de suivi exporté avec succès !",
|
||||||
|
"template.trackerEditorModal.messages.exportError": "Échec de l'exportation du preset. Vérifiez la console pour les détails.",
|
||||||
|
"template.trackerEditorModal.messages.importSuccess": "Preset de suivi importé avec succès !",
|
||||||
|
"template.trackerEditorModal.messages.importError": "Échec de l'importation du preset",
|
||||||
|
"template.trackerEditorModal.messages.importConfirm": "Ceci remplacera votre configuration actuelle de suivi. Continuer ?",
|
||||||
|
"template.trackerEditorModal.userStatsTab.customStatsTitle": "Stats Personnalisées",
|
||||||
|
"template.trackerEditorModal.userStatsTab.addCustomStatButton": "Ajouter Stat Perso",
|
||||||
|
"template.trackerEditorModal.userStatsTab.rpgAttributesTitle": "Attributs RPG",
|
||||||
|
"template.trackerEditorModal.userStatsTab.enableRpgAttributes": "Activer Section Attributs RPG",
|
||||||
|
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "Toujours Inclure Attributs dans Invite",
|
||||||
|
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "Si désactivé, les attributs ne sont envoyés que lorsqu'un lancer de dé est actif.",
|
||||||
|
"template.trackerEditorModal.userStatsTab.addAttributeButton": "Ajouter Attribut",
|
||||||
|
"template.trackerEditorModal.userStatsTab.statusSectionTitle": "Section Statut",
|
||||||
|
"template.trackerEditorModal.userStatsTab.enableStatusSection": "Activer Section Statut",
|
||||||
|
"template.trackerEditorModal.userStatsTab.showMoodEmoji": "Afficher Emoji Humeur",
|
||||||
|
"template.trackerEditorModal.userStatsTab.statusFieldsLabel": "Champs Statut (séparés par virgule) :",
|
||||||
|
"template.trackerEditorModal.userStatsTab.skillsSectionTitle": "Section Compétences",
|
||||||
|
"template.trackerEditorModal.userStatsTab.enableSkillsSection": "Activer Section Compétences",
|
||||||
|
"template.trackerEditorModal.userStatsTab.skillsLabelLabel": "Libellé Compétences :",
|
||||||
|
"template.trackerEditorModal.userStatsTab.skillsListLabel": "Liste Compétences (séparées par virgule) :",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.widgetsTitle": "Widgets",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.dateWidget": "Date",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.weatherWidget": "Météo",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.temperatureWidget": "Température",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.timeWidget": "Heure",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.locationWidget": "Lieu",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.recentEventsWidget": "Événements Récents",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.relationshipStatusTitle": "Champs Statut Relation",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.enableRelationshipStatus": "Activer Champs Statut Relation",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.relationshipStatusHint": "Définir les types de relation avec les emojis correspondants affichés sur les portraits des personnages.",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.newRelationshipButton": "Nouvelle Relation",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorTitle": "Champs Apparence/Comportement",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorHint": "Champs affichés sous le nom du personnage.",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.addCustomFieldButton": "Ajouter Champ Perso",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.thoughtsConfigTitle": "Configuration Pensées",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.enableCharacterThoughts": "Activer Pensées Personnage",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.thoughtsLabelLabel": "Libellé Pensées :",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "Instruction IA :",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Stats Personnage",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "Suivre Stats Personnage",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.characterStatsHint": "Créer des stats à suivre pour chaque personnage (affichées comme nombres colorés).",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "Ajouter Stat Personnage",
|
||||||
|
"template.mainPanel.title": "RPG Companion",
|
||||||
|
"template.mainPanel.lastRoll": "Dernier Lancer :",
|
||||||
|
"template.mainPanel.clearLastRoll": "Effacer dernier lancer",
|
||||||
|
"template.mainPanel.immersiveHtml": "HTML Immersif",
|
||||||
|
"template.mainPanel.coloredDialogues": "Dialogues Colorés",
|
||||||
|
"template.mainPanel.deceptionSystem": "Système Déception",
|
||||||
|
"template.mainPanel.omniscienceFilter": "Filtre Omniscience",
|
||||||
|
"template.mainPanel.spotifyMusic": "Musique Spotify",
|
||||||
|
"template.mainPanel.snowflakesEffect": "Effet Flocons",
|
||||||
|
"template.mainPanel.dynamicWeatherEffects": "Météo Dynamique",
|
||||||
|
"template.mainPanel.narratorMode": "Mode Narrateur",
|
||||||
|
"template.mainPanel.refreshRpgInfo": "Rafraîchir Infos RPG",
|
||||||
|
"template.mainPanel.updating": "Mise à jour...",
|
||||||
|
"template.mainPanel.editTrackersButton": "Éditer Suivis",
|
||||||
|
"template.mainPanel.settingsButton": "Paramètres",
|
||||||
|
"global.none": "Aucun",
|
||||||
|
"global.add": "Ajouter",
|
||||||
|
"global.cancel": "Annuler",
|
||||||
|
"global.listView": "Vue liste",
|
||||||
|
"global.gridView": "Vue grille",
|
||||||
|
"global.save": "Sauvegarder",
|
||||||
|
"global.status": "Statut",
|
||||||
|
"global.inventory": "Inventaire",
|
||||||
|
"global.quests": "Quêtes",
|
||||||
|
"global.info": "Info",
|
||||||
|
"infobox.noData.title": "Pas de données",
|
||||||
|
"infobox.noData.instruction": "Générez une nouvelle réponse dans le jeu de rôle ou basculez vers \"Génération Séparée\" dans les Paramètres pour accéder et cliquer sur le bouton \"Rafraîchir Infos RPG\"",
|
||||||
|
"infobox.recentEvents.title": "Événements Récents",
|
||||||
|
"infobox.recentEvents.addEventPlaceholder": "Ajouter événement...",
|
||||||
|
"inventory.section.onPerson": "Sur Soi",
|
||||||
|
"inventory.section.clothing": "Vêtements",
|
||||||
|
"inventory.section.stored": "Stocké",
|
||||||
|
"inventory.section.assets": "Biens",
|
||||||
|
"inventory.onPerson.empty": "Aucun objet porté",
|
||||||
|
"inventory.onPerson.title": "Objets Actuellement Portés",
|
||||||
|
"inventory.onPerson.addItemButton": "Ajouter Objet",
|
||||||
|
"inventory.onPerson.addItemPlaceholder": "Entrer nom objet...",
|
||||||
|
"inventory.clothing.empty": "Ne porte rien",
|
||||||
|
"inventory.clothing.title": "Vêtements & Armure",
|
||||||
|
"inventory.clothing.addItemButton": "Ajouter Vêtement",
|
||||||
|
"inventory.clothing.addItemPlaceholder": "Entrer vêtement...",
|
||||||
|
"inventory.stored.title": "Lieux de Stockage",
|
||||||
|
"inventory.stored.addLocationButton": "Ajouter Lieu",
|
||||||
|
"inventory.stored.addLocationPlaceholder": "Entrer nom lieu...",
|
||||||
|
"inventory.stored.saveButton": "Sauvegarder",
|
||||||
|
"inventory.stored.empty": "Aucun lieu de stockage. Cliquez sur \"Ajouter Lieu\" pour en créer un.",
|
||||||
|
"inventory.stored.noItems": "Aucun objet stocké ici",
|
||||||
|
"inventory.stored.addItemToLocationPlaceholder": "Entrer nom objet...",
|
||||||
|
"inventory.stored.addItemButton": "Ajouter Objet",
|
||||||
|
"inventory.stored.confirmRemoveLocationMessage": "Supprimer \"${location}\" ? Cela supprimera tous les objets stockés ici.",
|
||||||
|
"inventory.stored.confirmRemoveLocationConfirmButton": "Confirmer",
|
||||||
|
"inventory.assets.empty": "Aucun bien possédé",
|
||||||
|
"inventory.assets.title": "Véhicules, Propriétés & Possessions Majeures",
|
||||||
|
"inventory.assets.addAssetModalTitle": "Ajouter Bien",
|
||||||
|
"inventory.assets.addAssetButton": "Ajouter Bien",
|
||||||
|
"inventory.assets.addAssetPlaceholder": "Entrer nom bien...",
|
||||||
|
"inventory.assets.description": "Les biens incluent les véhicules (voitures, motos), les propriétés (maisons, appartements) et les équipements majeurs (outils d'atelier, objets spéciaux).",
|
||||||
|
"quests.section.main": "Quête Principale",
|
||||||
|
"quests.section.optional": "Quêtes Optionnelles",
|
||||||
|
"quests.main.title": "Quêtes Principales",
|
||||||
|
"quests.main.addQuestButton": "Ajouter Quête",
|
||||||
|
"quests.main.addQuestPlaceholder": "Entrer titre quête principale...",
|
||||||
|
"quests.main.empty": "Aucune quête principale active",
|
||||||
|
"quests.main.hint": "La quête principale représente votre objectif principal dans l'histoire.",
|
||||||
|
"quests.optional.title": "Quêtes Optionnelles",
|
||||||
|
"quests.optional.addQuestButton": "Ajouter Quête",
|
||||||
|
"quests.optional.addQuestPlaceholder": "Entrer titre quête optionnelle...",
|
||||||
|
"quests.optional.empty": "Aucune quête optionnelle active",
|
||||||
|
"quests.optional.hint": "Les quêtes optionnelles sont des objectifs secondaires qui complètent votre histoire principale.",
|
||||||
|
"checkpoint.setChapterStart": "Définir Début Chapitre",
|
||||||
|
"checkpoint.clearChapterStart": "Effacer Début Chapitre",
|
||||||
|
"checkpoint.indicator": "Début Chapitre",
|
||||||
|
"checkpoint.tooltip": "Les messages avant ce point sont exclus du contexte",
|
||||||
|
"musicPlayer.title": "Musique de Scène",
|
||||||
|
"musicPlayer.noMusic": "L'IA suggérera de la musique quand approprié pour la scène",
|
||||||
|
"errors.parsingError": "Erreur de parsing RPG Companion Trackers ! Le modèle a renvoyé un format incorrect. Si le problème persiste, envisagez de changer le modèle pour les générations.",
|
||||||
|
"settings.recommendedModels.title": "Modèles Recommandés",
|
||||||
|
"settings.recommendedModels.description": "Pour que l'extension fonctionne correctement, **il n'est pas recommandé d'utiliser des modèles de moins de 20B, surtout s'ils sont anciens.** Elle fonctionne mieux avec les modèles SOTA tels que Deepseek, Claude, GPT ou Gemini.",
|
||||||
|
"thoughts.addCharacter": "Ajouter un personnage",
|
||||||
|
"thoughts.locked": "Verrouillé",
|
||||||
|
"thoughts.unlocked": "Déverrouillé",
|
||||||
|
"thoughts.clickToEdit": "Cliquer pour modifier",
|
||||||
|
"thoughts.clickToUpload": "Cliquer pour télécharger un avatar",
|
||||||
|
"thoughts.removeCharacter": "Supprimer le personnage",
|
||||||
|
"userStats.level": "NIV",
|
||||||
|
"userStats.clickToEditLevel": "Cliquer pour modifier le niveau",
|
||||||
|
"userStats.statsLocked": "Verrouillé - L'IA ne peut pas modifier les stats",
|
||||||
|
"userStats.statsUnlocked": "Déverrouillé - L'IA peut modifier les stats",
|
||||||
|
"userStats.clickToEditStatName": "Cliquer pour modifier le nom",
|
||||||
|
"userStats.clickToEditStatValue": "Cliquer pour modifier",
|
||||||
|
"userStats.moodLocked": "Verrouillé - L'IA ne peut pas modifier l'humeur",
|
||||||
|
"userStats.moodUnlocked": "Déverrouillé - L'IA peut modifier l'humeur",
|
||||||
|
"userStats.clickToEditEmoji": "Cliquer pour modifier l'émoji",
|
||||||
|
"userStats.skillsLocked": "Verrouillé - L'IA ne peut pas modifier les compétences",
|
||||||
|
"userStats.skillsUnlocked": "Déverrouillé - L'IA peut modifier les compétences",
|
||||||
|
"userStats.clickToEditSkills": "Cliquer pour modifier les compétences",
|
||||||
|
"infoBox.clickToEdit": "Cliquer pour modifier",
|
||||||
|
"infoBox.locked": "Verrouillé - L'IA ne peut pas modifier ceci",
|
||||||
|
"infoBox.unlocked": "Déverrouillé - L'IA peut modifier ceci",
|
||||||
|
"infoBox.weatherFallback": "Météo",
|
||||||
|
"infoBox.locationFallback": "Lieu",
|
||||||
|
"stats.health": "Santé",
|
||||||
|
"stats.satiety": "Satiété",
|
||||||
|
"stats.energy": "Énergie",
|
||||||
|
"stats.hygiene": "Hygiène",
|
||||||
|
"stats.arousal": "Excitation",
|
||||||
|
"stats.str": "FOR",
|
||||||
|
"stats.dex": "DEX",
|
||||||
|
"stats.con": "CON",
|
||||||
|
"stats.int": "INT",
|
||||||
|
"stats.wis": "VOL",
|
||||||
|
"stats.cha": "CHA"
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
{
|
||||||
|
"settings.language.label": "Язык",
|
||||||
|
"settings.language.option.en": "English",
|
||||||
|
"settings.language.option.zh-cn": "简体中文",
|
||||||
|
"settings.language.option.zh-tw": "繁體中文",
|
||||||
|
"settings.language.option.ru": "Русский",
|
||||||
|
"settings.extensionEnabled": "Включить RPG Companion",
|
||||||
|
"settings.note": "Включить или отключить расширение RPG Companion. Дополнительные настройки производятся непосредственно в панели приложения.",
|
||||||
|
"template.settingsTitle": "Настройки RPG Companion",
|
||||||
|
"template.settingsModal.themeTitle": "Тема",
|
||||||
|
"template.settingsModal.themeLabel": "Стиль:",
|
||||||
|
"template.settingsModal.themeOptions.default": "По умолчанию",
|
||||||
|
"template.settingsModal.themeOptions.sciFi": "Скай-фай (Synthwave)",
|
||||||
|
"template.settingsModal.themeOptions.fantasy": "Фэнтези (Rustic Parchment)",
|
||||||
|
"template.settingsModal.themeOptions.cyberpunk": "Киберпанк (Neon Grid)",
|
||||||
|
"template.settingsModal.themeOptions.custom": "Своя",
|
||||||
|
"template.settingsModal.themeOptions.custom.background": "Фон:",
|
||||||
|
"template.settingsModal.themeOptions.custom.accent": "Акцент:",
|
||||||
|
"template.settingsModal.themeOptions.custom.text": "Текст:",
|
||||||
|
"template.settingsModal.themeOptions.custom.highlight": "Подсветка:",
|
||||||
|
"template.settingsModal.theme.statBarLow": "Цвет полоски характеристики (Низкие значения):",
|
||||||
|
"template.settingsModal.theme.statBarLowNote": "Цвет при значении показателей 0%.",
|
||||||
|
"template.settingsModal.theme.statBarHigh": "Цвет полоски характеристики (Высокие значения):",
|
||||||
|
"template.settingsModal.theme.statBarHighNote": "Цвет при значении показателей 100%.",
|
||||||
|
"template.settingsModal.displayTitle": "Настройки отображения",
|
||||||
|
"template.settingsModal.displayNote": "Вы можете вкючить/отключить расширение RPG Companion во вкладке расширений для SillyTavern.",
|
||||||
|
"template.settingsModal.display.panelPosition": "Положение боковой панели:",
|
||||||
|
"template.settingsModal.display.panelPositionOptions.right": "Справа",
|
||||||
|
"template.settingsModal.display.panelPositionOptions.left": "Слева",
|
||||||
|
"template.settingsModal.display.toggleAutoUpdate": "Авто-обновление после ответа",
|
||||||
|
"template.settingsModal.display.toggleAutoUpdateNote": "Автоматически обновлять информацию в трекрере после каждого ответа.",
|
||||||
|
"template.settingsModal.display.showUserStats": "Показать Характеристики Игрока",
|
||||||
|
"template.settingsModal.display.showUserStatsNote": "Включить Характеристики Игрока, которые отслеживают статистику используемой персоны - характеристики, настроение, навыки и т.д.",
|
||||||
|
"template.settingsModal.display.showInfoBox": "Показывать Инфо-панель",
|
||||||
|
"template.settingsModal.display.showInfoBoxNote": "Отображение локации, времени, погоды и недавних событий.",
|
||||||
|
"template.settingsModal.display.showPresentCharacters": "Показывать персонажей",
|
||||||
|
"template.settingsModal.display.showPresentCharactersNote": "Показывать портреты персонажей с их текущимы мыслями и статусом.",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharacters": "Показывать персонажей под чатом",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharactersNote": "Показывать компактную панель персонажей под чатом.",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressions": "Выражения на основе мыслей",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressionsNote": "Использовать Character Expressions в SillyTavern для классификации мыслей каждого присутствующего персонажа в панели под чатом. Расход токенов может увеличиться в зависимости от выбранного API классификации.",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplay": "Скрыть отображение выражений по умолчанию",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "Скрыть встроенное отображение выражений персонажей SillyTavern.",
|
||||||
|
"template.settingsModal.display.narratorMode": "Режим расказчика",
|
||||||
|
"template.settingsModal.display.narratorModeNote": "Использовать карточку персонажа в качестве расказчика. Персонажи берутся из контекста вместо фиксированных отсылок.",
|
||||||
|
"template.settingsModal.display.showInventory": "Показывать инвентарь",
|
||||||
|
"template.settingsModal.display.showInventoryNote": "Отслеживайте переносимые предметы, одежду, хранимые вещи и активы.",
|
||||||
|
"template.settingsModal.display.showQuests": "Показывать задания",
|
||||||
|
"template.settingsModal.display.showQuestsNote": "Управляйте основными и дополнительными заданиями с целями.",
|
||||||
|
"template.settingsModal.display.showLockIcons": "Показывать значки блокировки/разблокировки трекеров",
|
||||||
|
"template.settingsModal.display.showLockIconsNote": "Отображать значки блокировки/разблокировки на элементах трекера, чтобы предотвратить их изменение ИИ.",
|
||||||
|
"template.settingsModal.display.showThoughtsInChat": "Показывать мысли",
|
||||||
|
"template.settingsModal.display.showThoughtsInChatNote": "Отображать мысли персонажей в виде всплывающих пузырьков рядом с их сообщениями.",
|
||||||
|
"template.settingsModal.display.showInlineThoughts": "Показывать мысли под текстом сообщения",
|
||||||
|
"template.settingsModal.display.showInlineThoughtsNote": "Переключает между стандартными угловыми пузырями мыслей и карточками мыслей под текстом сообщения.",
|
||||||
|
"template.settingsModal.display.alwaysShowThoughtBubble": "Всегда показывать пузырь мыслей",
|
||||||
|
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "Автоматически раскрывать пузырь мыслей без предварительного нажатия на значок",
|
||||||
|
"template.settingsModal.display.enableAnimations": "Включить анимации",
|
||||||
|
"template.settingsModal.display.enableAnimationsNote": "Плавные переходы для характеристик, обновления контента и бросков кубиков.",
|
||||||
|
"template.settingsModal.display.showImmersiveHtmlToggle": "Показывать переключатель Immersive HTML",
|
||||||
|
"template.settingsModal.display.showImmersiveHtmlToggleNote": "Отображать кнопку переключения для включения/отключения HTML-форматирования в сообщениях.",
|
||||||
|
"template.settingsModal.display.showDialogueColoringToggle": "Показывать переключатель цветных диалогов",
|
||||||
|
"template.settingsModal.display.showDialogueColoringToggleNote": "Отображать кнопку переключения для включения/отключения цветного форматирования диалогов.",
|
||||||
|
"template.settingsModal.display.showSpotifyMusicToggle": "Показывать переключатель музыки Spotify",
|
||||||
|
"template.settingsModal.display.showSpotifyMusicToggleNote": "Отображать музыкальный проигрыватель Spotify с предложенными ИИ треками, подходящими для сцены.",
|
||||||
|
"template.settingsModal.display.showSnowflakesToggle": "Показывать переключатель погодных эффектов",
|
||||||
|
"template.settingsModal.display.showDynamicWeatherToggle": "Показывать переключатель динамических погодных эффектов",
|
||||||
|
"template.settingsModal.display.showDynamicWeatherToggleNote": "Отображать кнопку переключения для включения/отключения анимированных погодных эффектов.",
|
||||||
|
"template.settingsModal.display.showNarratorMode": "Показывать переключатель режима рассказчика",
|
||||||
|
"template.settingsModal.display.showNarratorModeNote": "Отображать кнопку переключения для включения/отключения режима рассказчика (персонажи определяются из контекста).",
|
||||||
|
"template.settingsModal.display.showAutoAvatars": "Показывать переключатель автоматической генерации аватаров",
|
||||||
|
"template.settingsModal.display.showAutoAvatarsNote": "Отображать кнопку переключения для автоматической генерации аватаров для персонажей без изображений.",
|
||||||
|
"template.settingsModal.display.showRandomizedPlot": "Показывать переключатель случайного развития сюжета",
|
||||||
|
"template.settingsModal.display.showRandomizedPlotNote": "Отображать кнопку для генерации ИИ случайных подсказок для развития сюжета.",
|
||||||
|
"template.settingsModal.display.showNaturalPlot": "Показывать переключатель естественного развития сюжета",
|
||||||
|
"template.settingsModal.display.showNaturalPlotNote": "Отображать кнопку для контекстно-зависимых подсказок продолжения повествования.",
|
||||||
|
"template.settingsModal.display.showStartEncounter": "Показывать переключатель начала встречи",
|
||||||
|
"template.settingsModal.display.showStartEncounterNote": "Отображать кнопку для начала интерактивных боевых столкновений.",
|
||||||
|
"template.settingsModal.display.showDiceDisplay": "Показывать отображение броска кубиков",
|
||||||
|
"template.settingsModal.display.showDiceDisplayNote": "Отображать индикатор \"Последний бросок\" на панели.",
|
||||||
|
"template.mainPanel.autoAvatars": "Авто-аватары",
|
||||||
|
"template.settingsModal.advancedTitle": "Дополнительно",
|
||||||
|
"template.settingsModal.advanced.encounterHistoryDepth": "Глубина истории чата для боя:",
|
||||||
|
"template.settingsModal.advanced.encounterHistoryDepthNote": "Количество последних сообщений, включаемых при инициализации боя.",
|
||||||
|
"template.settingsModal.advanced.autoSaveCombatLogs": "Автосохранение журналов боя",
|
||||||
|
"template.settingsModal.advanced.autoSaveCombatLogsNote": "Сохранять подробные журналы боя в файл для будущего использования и анализа.",
|
||||||
|
"template.settingsModal.advanced.clearCacheNote": "Очищает сохраненные и отображаемые данные трекеров для текущего активного чата.",
|
||||||
|
"template.settingsModal.advanced.generationMode": "Режим генерации:",
|
||||||
|
"template.settingsModal.advanced.generationModeOptions.together": "Вместе с основной генерацией",
|
||||||
|
"template.settingsModal.advanced.generationModeOptions.separate": "Отдельная генерация",
|
||||||
|
"template.settingsModal.advanced.generationModeNote": "Вместе: добавляет RPG-трекинг к основному ответу. Отдельно: генерирует RPG-данные отдельно (вручную или автоматически). Внешний: подключается напрямую к OpenAI-совместимому эндпоинту.",
|
||||||
|
"template.settingsModal.advanced.generationModeOptions.external": "Внешний API",
|
||||||
|
"template.settingsModal.advanced.externalApi.title": "Настройки внешнего API",
|
||||||
|
"template.settingsModal.advanced.externalApi.baseUrl": "Базовый URL API",
|
||||||
|
"template.settingsModal.advanced.externalApi.baseUrlNote": "OpenAI-совместимый эндпоинт (например, OpenAI, OpenRouter, локальный сервер LLM).",
|
||||||
|
"template.settingsModal.advanced.externalApi.apiKey": "API-ключ",
|
||||||
|
"template.settingsModal.advanced.externalApi.apiKeyNote": "Ваш API-ключ для внешнего сервиса.",
|
||||||
|
"template.settingsModal.advanced.externalApi.model": "Модель",
|
||||||
|
"template.settingsModal.advanced.externalApi.modelNote": "Идентификатор модели (например, gpt-4o-mini, claude-3-haiku, mistral-7b).",
|
||||||
|
"template.settingsModal.advanced.externalApi.maxTokens": "Максимальное количество токенов",
|
||||||
|
"template.settingsModal.advanced.externalApi.temperature": "Температура",
|
||||||
|
"template.settingsModal.advanced.externalApi.testConnection": "Тестировать соединение",
|
||||||
|
"template.settingsModal.advanced.contextMessages": "Контекстные сообщения:",
|
||||||
|
"template.settingsModal.advanced.contextMessagesNote": "Количество последних сообщений, включаемых в контекст.",
|
||||||
|
"template.settingsModal.advanced.useSeparatePreset": "Использовать модель, подключенную к пресету RPG Companion Trackers",
|
||||||
|
"template.settingsModal.advanced.useSeparatePresetNote": "При включении генерация трекеров будет использовать модель из пресета \"RPG Companion Trackers\" вместо основной модели API. Пресет будет автоматически переключаться во время генерации и восстанавливаться после нее. Выберите желаемую модель в этом пресете и убедитесь, что переключатель \"Bind presets to API connections\" включен (рядом с кнопками импорта/экспорта пресетов).",
|
||||||
|
"template.settingsModal.advanced.skipInjections": "Пропускать инъекции во время управляемых генераций:",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsOptions.none": "Никогда не пропускать",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsOptions.impersonation": "Только при запросах олицетворения",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsOptions.guided": "Всегда для управляемых или тихих подсказок",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsNote": "При установке расширение не будет внедрять подсказки трекеров, примеры или HTML-инструкции в соответствии с выбранным режимом при обнаружении управляемой генерации (через `instruct` или `quiet_prompt`). Полезно при использовании GuidedGenerations или аналогичных расширений.",
|
||||||
|
"template.settingsModal.advanced.customHtmlPromptTitle": "Пользовательская HTML-подсказка:",
|
||||||
|
"template.settingsModal.advanced.restoreDefaultHtmlPrompt": "Восстановить по умолчанию",
|
||||||
|
"template.settingsModal.advanced.customHtmlPromptNote": "Настройте HTML-подсказку, которая внедряется при включенной опции \"Enable Immersive HTML\". Подсказка по умолчанию показана выше - вы можете редактировать ее напрямую или полностью заменить. Нажмите \"Восстановить по умолчанию\" для сброса. Это влияет на все режимы генерации (together, separate и plot progression).",
|
||||||
|
"template.settingsModal.advanced.clearCache": "Очистить кэш расширения",
|
||||||
|
"template.settingsModal.advanced.resetFabPositions": "Сбросить позиции кнопок",
|
||||||
|
"template.settingsModal.advanced.resetFabPositionsNote": "Сбрасывает все плавающие кнопки действий (переключение, обновление, отладка) в позиции по умолчанию (сверху слева). Полезно, если кнопки находятся за пределами экрана.",
|
||||||
|
"template.trackerEditorModal.title": "Редактировать трекеры",
|
||||||
|
"template.trackerEditorModal.tabs.userStats": "Характеристики пользователя",
|
||||||
|
"template.trackerEditorModal.tabs.infoBox": "Инфо-панель",
|
||||||
|
"template.trackerEditorModal.tabs.presentCharacters": "Присутствующие персонажи",
|
||||||
|
"template.trackerEditorModal.buttons.reset": "Сбросить",
|
||||||
|
"template.trackerEditorModal.buttons.cancel": "Отмена",
|
||||||
|
"template.trackerEditorModal.buttons.save": "Сохранить и применить",
|
||||||
|
"template.trackerEditorModal.buttons.export": "Экспорт",
|
||||||
|
"template.trackerEditorModal.buttons.import": "Импорт",
|
||||||
|
"template.trackerEditorModal.messages.exportSuccess": "Шаблон трекеров успешно экспортирован!",
|
||||||
|
"template.trackerEditorModal.messages.exportError": "Не удалось экспортировать шаблон трекеров. Проверьте консоль для получения подробностей.",
|
||||||
|
"template.trackerEditorModal.messages.importSuccess": "Шаблон трекеров успешно импортирован!",
|
||||||
|
"template.trackerEditorModal.messages.importError": "Не удалось импортировать шаблон трекеров",
|
||||||
|
"template.trackerEditorModal.messages.importConfirm": "Это заменит текущую конфигурацию трекеров. Продолжить?",
|
||||||
|
"template.trackerEditorModal.userStatsTab.customStatsTitle": "Пользовательские характеристики",
|
||||||
|
"template.trackerEditorModal.userStatsTab.addCustomStatButton": "Добавить пользовательскую характеристику",
|
||||||
|
"template.trackerEditorModal.userStatsTab.rpgAttributesTitle": "RPG-атрибуты",
|
||||||
|
"template.trackerEditorModal.userStatsTab.enableRpgAttributes": "Включить раздел RPG-атрибутов",
|
||||||
|
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "Всегда включать атрибуты в подсказку",
|
||||||
|
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "Если отключено, атрибуты отправляются только при активном броске кубиков.",
|
||||||
|
"template.trackerEditorModal.userStatsTab.addAttributeButton": "Добавить атрибут",
|
||||||
|
"template.trackerEditorModal.userStatsTab.statusSectionTitle": "Раздел статуса",
|
||||||
|
"template.trackerEditorModal.userStatsTab.enableStatusSection": "Включить раздел статуса",
|
||||||
|
"template.trackerEditorModal.userStatsTab.showMoodEmoji": "Показывать эмодзи настроения",
|
||||||
|
"template.trackerEditorModal.userStatsTab.statusFieldsLabel": "Поля статуса (через запятую):",
|
||||||
|
"template.trackerEditorModal.userStatsTab.skillsSectionTitle": "Раздел навыков",
|
||||||
|
"template.trackerEditorModal.userStatsTab.enableSkillsSection": "Включить раздел навыков",
|
||||||
|
"template.trackerEditorModal.userStatsTab.skillsLabelLabel": "Метка навыков:",
|
||||||
|
"template.trackerEditorModal.userStatsTab.skillsListLabel": "Список навыков (через запятую):",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.widgetsTitle": "Виджеты",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.dateWidget": "Дата",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.weatherWidget": "Погода",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.temperatureWidget": "Температура",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.timeWidget": "Время",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.locationWidget": "Местоположение",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.recentEventsWidget": "Недавние события",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.relationshipStatusTitle": "Поля статуса отношений",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.enableRelationshipStatus": "Включить поля статуса отношений",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.relationshipStatusHint": "Определите типы отношений с соответствующими эмодзи, отображаемыми на портретах персонажей.",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.newRelationshipButton": "Новое отношение",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorTitle": "Поля внешности/поведения",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorHint": "Поля, отображаемые под именем персонажа.",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.addCustomFieldButton": "Добавить пользовательское поле",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.thoughtsConfigTitle": "Настройки мыслей",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.enableCharacterThoughts": "Включить мысли персонажей",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.thoughtsLabelLabel": "Метка мыслей:",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "Инструкция для ИИ:",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "Характеристики персонажей",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "Отслеживать характеристики персонажей",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.characterStatsHint": "Создавайте характеристики для отслеживания для каждого персонажа (отображаются в виде цветных полос).",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "Добавить характеристику персонажа",
|
||||||
|
"template.mainPanel.title": "RPG Companion",
|
||||||
|
"template.mainPanel.lastRoll": "Последний бросок:",
|
||||||
|
"template.mainPanel.clearLastRoll": "Очистить последний бросок",
|
||||||
|
"template.mainPanel.immersiveHtml": "Immersive HTML",
|
||||||
|
"template.mainPanel.coloredDialogues": "Цветные диалоги",
|
||||||
|
"template.mainPanel.spotifyMusic": "Музыка Spotify",
|
||||||
|
"template.mainPanel.snowflakesEffect": "Эффект снежинок",
|
||||||
|
"template.mainPanel.dynamicWeatherEffects": "Динамическая погода",
|
||||||
|
"template.mainPanel.narratorMode": "Режим рассказчика",
|
||||||
|
"template.mainPanel.refreshRpgInfo": "Обновить RPG-информацию",
|
||||||
|
"template.mainPanel.updating": "Обновление...",
|
||||||
|
"template.mainPanel.editTrackersButton": "Редактировать трекеры",
|
||||||
|
"template.mainPanel.settingsButton": "Настройки",
|
||||||
|
"global.none": "Нет",
|
||||||
|
"global.add": "Добавить",
|
||||||
|
"global.cancel": "Отмена",
|
||||||
|
"global.listView": "Вид списка",
|
||||||
|
"global.gridView": "Вид сетки",
|
||||||
|
"global.save": "Сохранить",
|
||||||
|
"global.status": "Статус",
|
||||||
|
"global.inventory": "Инвентарь",
|
||||||
|
"global.quests": "Задания",
|
||||||
|
"global.info": "Информация",
|
||||||
|
"infobox.noData.title": "Данных пока нет",
|
||||||
|
"infobox.noData.instruction": "Сгенерируйте новый ответ в ролевой игре или переключитесь на \"Отдельную генерацию\" в Настройках, чтобы получить доступ и нажать кнопку \"Обновить RPG-информацию\"",
|
||||||
|
"infobox.recentEvents.title": "Недавние события",
|
||||||
|
"infobox.recentEvents.addEventPlaceholder": "Добавить событие...",
|
||||||
|
"inventory.section.onPerson": "При себе",
|
||||||
|
"inventory.section.clothing": "Одежда",
|
||||||
|
"inventory.section.stored": "Хранимое",
|
||||||
|
"inventory.section.assets": "Активы",
|
||||||
|
"inventory.onPerson.empty": "Нет переносимых предметов",
|
||||||
|
"inventory.onPerson.title": "Предметы, которые сейчас в инвентаре",
|
||||||
|
"inventory.onPerson.addItemButton": "Добавить предмет",
|
||||||
|
"inventory.onPerson.addItemPlaceholder": "Введите название предмета...",
|
||||||
|
"inventory.clothing.empty": "Ничего не надето",
|
||||||
|
"inventory.clothing.title": "Одежда и броня",
|
||||||
|
"inventory.clothing.addItemButton": "Добавить одежду",
|
||||||
|
"inventory.clothing.addItemPlaceholder": "Введите элемент одежды...",
|
||||||
|
"inventory.stored.title": "Места хранения",
|
||||||
|
"inventory.stored.addLocationButton": "Добавить место",
|
||||||
|
"inventory.stored.addLocationPlaceholder": "Введите название места...",
|
||||||
|
"inventory.stored.saveButton": "Сохранить",
|
||||||
|
"inventory.stored.empty": "Пока нет мест хранения. Нажмите \"Добавить место\", чтобы создать.",
|
||||||
|
"inventory.stored.noItems": "Здесь нет хранимых предметов",
|
||||||
|
"inventory.stored.addItemToLocationPlaceholder": "Введите название предмета...",
|
||||||
|
"inventory.stored.addItemButton": "Добавить предмет",
|
||||||
|
"inventory.stored.confirmRemoveLocationMessage": "Удалить \"${location}\"? Это удалит все предметы, хранящиеся там.",
|
||||||
|
"inventory.stored.confirmRemoveLocationConfirmButton": "Подтвердить",
|
||||||
|
"inventory.assets.empty": "Нет активов",
|
||||||
|
"inventory.assets.title": "Транспорт, недвижимость и крупные владения",
|
||||||
|
"inventory.assets.addAssetModalTitle": "Добавить актив",
|
||||||
|
"inventory.assets.addAssetButton": "Добавить актив",
|
||||||
|
"inventory.assets.addAssetPlaceholder": "Введите название актива...",
|
||||||
|
"inventory.assets.description": "Активы включают транспортные средства (автомобили, мотоциклы), недвижимость (дома, квартиры) и крупное оборудование (инструменты для мастерской, специальные предметы).",
|
||||||
|
"quests.section.main": "Основное задание",
|
||||||
|
"quests.section.optional": "Дополнительные задания",
|
||||||
|
"quests.main.title": "Основные задания",
|
||||||
|
"quests.main.addQuestButton": "Добавить задание",
|
||||||
|
"quests.main.addQuestPlaceholder": "Введите название основного задания...",
|
||||||
|
"quests.main.empty": "Нет активных основных заданий",
|
||||||
|
"quests.main.hint": "Основное задание представляет вашу главную цель в истории.",
|
||||||
|
"quests.optional.title": "Дополнительные задания",
|
||||||
|
"quests.optional.addQuestButton": "Добавить задание",
|
||||||
|
"quests.optional.addQuestPlaceholder": "Введите название дополнительного задания...",
|
||||||
|
"quests.optional.empty": "Нет активных дополнительных заданий",
|
||||||
|
"quests.optional.hint": "Дополнительные задания - это побочные цели, которые дополняют основную историю.",
|
||||||
|
"checkpoint.setChapterStart": "Установить начало главы",
|
||||||
|
"checkpoint.clearChapterStart": "Очистить начало главы",
|
||||||
|
"checkpoint.indicator": "Начало главы",
|
||||||
|
"checkpoint.tooltip": "Сообщения до этой точки исключаются из контекста",
|
||||||
|
"musicPlayer.title": "Музыка сцены",
|
||||||
|
"musicPlayer.noMusic": "ИИ будет предлагать музыку, когда это уместно для сцены",
|
||||||
|
"errors.parsingError": "Ошибка парсинга RPG Companion Trackers! Модель вернула неправильный формат. Если проблема сохраняется, рассмотрите возможность смены модели для генераций.",
|
||||||
|
"settings.recommendedModels.title": "Рекомендуемые модели",
|
||||||
|
"settings.recommendedModels.description": "Для правильной работы расширения **не рекомендуется использовать модели с базой обчучения ниже 20B, особенно если они старые.** Оно лучше всего работает с современными моделями, такими как Deepseek, Claude, GPT или Gemini."
|
||||||
|
}
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
const glob = require('glob');
|
||||||
|
|
||||||
|
const COMPILED_DIR = __dirname // path.join(__dirname, 'compiled');
|
||||||
|
|
||||||
|
function findUnlocalizedText() {
|
||||||
|
const srcArg = process.argv.find(arg => arg.startsWith('--src='));
|
||||||
|
const srcDir = srcArg ? srcArg.split('=')[1] : '.';
|
||||||
|
|
||||||
|
console.log(`\n🔎 Scanning for unlocalized text in ${srcDir}...`);
|
||||||
|
|
||||||
|
const files = glob.sync(`${srcDir}/**/*.{html,js,jsx}`, {
|
||||||
|
ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.log('⚠️ No .html/.js/.jsx files found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalFound = 0;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const content = fs.readFileSync(file, 'utf8');
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const relPath = path.relative(process.cwd(), file);
|
||||||
|
|
||||||
|
// Searching for string number
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
let match;
|
||||||
|
const localPattern = /<([a-zA-Z][a-zA-Z0-9]*)(?:\s(?:[^>](?!data-i18n-key))*)?>([\p{L}\p{N}\s\-.,!?:'"()]+)<\/\1>/gu;
|
||||||
|
|
||||||
|
while ((match = localPattern.exec(line)) !== null) {
|
||||||
|
const text = match[2].trim();
|
||||||
|
if (!text) continue;
|
||||||
|
|
||||||
|
// Passing JSX expressions like {someVar}
|
||||||
|
if (text.includes('{') || text.includes('}')) continue;
|
||||||
|
|
||||||
|
// Passing if tag has data-i18n-key
|
||||||
|
if (match[0].includes('data-i18n-key')) continue;
|
||||||
|
|
||||||
|
console.log(` - ${relPath}:${index + 1} — <${match[1]}> "${text}"`);
|
||||||
|
totalFound++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalFound === 0) {
|
||||||
|
console.log('✅ No unlocalized text found!');
|
||||||
|
} else {
|
||||||
|
console.log(`\n📋 Found ${totalFound} potentially unlocalized text node(s)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Function to validate translations
|
||||||
|
function validateTranslations() {
|
||||||
|
console.log('🔍 Validating translation files...');
|
||||||
|
|
||||||
|
// Parse --locales=en,fr argument
|
||||||
|
const localesArg = process.argv.find(arg => arg.startsWith('--locales='));
|
||||||
|
const selectedLocales = localesArg
|
||||||
|
? localesArg.split('=')[1].split(',').map(l => l.trim())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const files = fs.readdirSync(COMPILED_DIR)
|
||||||
|
.filter(file => file.endsWith('.json'))
|
||||||
|
.filter(file => {
|
||||||
|
const locale = path.basename(file, '.json');
|
||||||
|
return !selectedLocales || selectedLocales.includes(locale);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.log('⚠️ No compiled translation files found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all translation data
|
||||||
|
const translations = {};
|
||||||
|
for (const file of files) {
|
||||||
|
const locale = path.basename(file, '.json');
|
||||||
|
const filePath = path.join(COMPILED_DIR, file);
|
||||||
|
translations[locale] = fs.readJsonSync(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all locales
|
||||||
|
const locales = Object.keys(translations);
|
||||||
|
console.log(`📁 Found ${locales.length} locales: ${locales.join(', ')}`);
|
||||||
|
|
||||||
|
if (locales.length < 2) {
|
||||||
|
console.log('⚠️ Need at least 2 locales to compare');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose the first locale as reference
|
||||||
|
const referenceLocale = locales[0];
|
||||||
|
console.log(`🔑 Using ${referenceLocale} as reference locale`);
|
||||||
|
|
||||||
|
// Get all keys from reference locale
|
||||||
|
const referenceKeys = Object.keys(translations[referenceLocale]);
|
||||||
|
console.log(`🔢 Reference locale has ${referenceKeys.size} unique keys`);
|
||||||
|
|
||||||
|
// Track statistics
|
||||||
|
const stats = {
|
||||||
|
missingKeys: {},
|
||||||
|
extraKeys: {},
|
||||||
|
typeErrors: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize stats for each locale
|
||||||
|
for (const locale of locales) {
|
||||||
|
if (locale !== referenceLocale) {
|
||||||
|
stats.missingKeys[locale] = [];
|
||||||
|
stats.extraKeys[locale] = [];
|
||||||
|
stats.typeErrors[locale] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each locale against the reference
|
||||||
|
for (const locale of locales) {
|
||||||
|
if (locale === referenceLocale) continue;
|
||||||
|
|
||||||
|
const localeKeys = Object.keys(translations[locale]);
|
||||||
|
|
||||||
|
// Check for missing keys
|
||||||
|
for (const key of referenceKeys) {
|
||||||
|
if (!key in translations[locale]) {
|
||||||
|
stats.missingKeys[locale].push(key);
|
||||||
|
} else {
|
||||||
|
// Check for type mismatches
|
||||||
|
const refValue = translations[referenceLocale][key];
|
||||||
|
const localeValue = translations[locale][key];
|
||||||
|
|
||||||
|
if (typeof refValue !== typeof localeValue) {
|
||||||
|
stats.typeErrors[locale].push({
|
||||||
|
key,
|
||||||
|
refType: typeof refValue,
|
||||||
|
localeType: typeof localeValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for extra keys
|
||||||
|
for (const key of localeKeys) {
|
||||||
|
if (!key in translations[referenceLocale]) {
|
||||||
|
stats.extraKeys[locale].push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print results
|
||||||
|
let hasIssues = false;
|
||||||
|
|
||||||
|
// Print missing keys
|
||||||
|
for (const locale in stats.missingKeys) {
|
||||||
|
const missing = stats.missingKeys[locale];
|
||||||
|
if (missing.length > 0) {
|
||||||
|
hasIssues = true;
|
||||||
|
console.log(`❌ ${locale} is missing ${missing.length} keys:`);
|
||||||
|
missing.forEach(key => {
|
||||||
|
console.log(` - ${key}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print extra keys
|
||||||
|
for (const locale in stats.extraKeys) {
|
||||||
|
const extra = stats.extraKeys[locale];
|
||||||
|
if (extra.length > 0) {
|
||||||
|
hasIssues = true;
|
||||||
|
console.log(`⚠️ ${locale} has ${extra.length} extra keys:`);
|
||||||
|
extra.forEach(key => {
|
||||||
|
console.log(` - ${key}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print type errors
|
||||||
|
for (const locale in stats.typeErrors) {
|
||||||
|
const typeErrors = stats.typeErrors[locale];
|
||||||
|
if (typeErrors.length > 0) {
|
||||||
|
hasIssues = true;
|
||||||
|
console.log(`⚠️ ${locale} has ${typeErrors.length} type mismatches:`);
|
||||||
|
typeErrors.forEach(err => {
|
||||||
|
console.log(` - ${err.key}: expected ${err.refType}, got ${err.localeType}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print empty values check if needed
|
||||||
|
console.log('\n📊 Checking for empty values...');
|
||||||
|
for (const locale of locales) {
|
||||||
|
checkEmptyValues(translations[locale], locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasIssues) {
|
||||||
|
console.log('✅ All locales have consistent structure!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasIssues;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check for empty values
|
||||||
|
function checkEmptyValues(obj, locale, prefix = '') {
|
||||||
|
for (const key in obj) {
|
||||||
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||||
|
const value = obj[key];
|
||||||
|
|
||||||
|
if (value === '') {
|
||||||
|
console.log(`⚠️ ${locale} has empty string at ${fullKey}`);
|
||||||
|
} else if (value === null) {
|
||||||
|
console.log(`⚠️ ${locale} has null value at ${fullKey}`);
|
||||||
|
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
checkEmptyValues(value, locale, fullKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main function
|
||||||
|
function main() {
|
||||||
|
// Create compiled directory if it doesn't exist
|
||||||
|
fs.ensureDirSync(COMPILED_DIR);
|
||||||
|
|
||||||
|
// Run validation
|
||||||
|
validateTranslations();
|
||||||
|
|
||||||
|
// Find unlocalized text
|
||||||
|
findUnlocalizedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch mode
|
||||||
|
if (process.argv.includes('--watch')) {
|
||||||
|
console.log('👀 Watching for changes...');
|
||||||
|
|
||||||
|
let debounceTimer;
|
||||||
|
const debounceDelay = 100;
|
||||||
|
|
||||||
|
// Initial validation
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
main();
|
||||||
|
}, debounceDelay);
|
||||||
|
|
||||||
|
// Watch for changes in the compiled directory
|
||||||
|
chokidar.watch(COMPILED_DIR, {
|
||||||
|
ignoreInitial: true,
|
||||||
|
ignored: /.*~$/, // Игнорировать скрытые файлы
|
||||||
|
}).on('all', (event, path) => {
|
||||||
|
if (event === 'change' || event === 'add' || event === 'unlink') {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
console.log(`🔁 Detected changes in ${path} (${event}), revalidating...`);
|
||||||
|
main();
|
||||||
|
}, debounceDelay);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Run once
|
||||||
|
main();
|
||||||
|
}
|
||||||
@@ -0,0 +1,493 @@
|
|||||||
|
{
|
||||||
|
"settings.language.label": "语言",
|
||||||
|
"settings.language.option.en": "English",
|
||||||
|
"settings.language.option.zh-cn": "简体中文",
|
||||||
|
"settings.language.option.zh-tw": "繁體中文",
|
||||||
|
"settings.language.option.ru": "Русский",
|
||||||
|
"settings.language.option.fr": "Français",
|
||||||
|
"settings.extensionEnabled": "启用 RPG Companion",
|
||||||
|
"settings.note": "切换以启用/禁用 RPG Companion 扩展。其他设置可在面板内配置。",
|
||||||
|
"template.settingsTitle": "RPG Companion 设置",
|
||||||
|
"template.settingsModal.themeTitle": "主题",
|
||||||
|
"template.settingsModal.themeLabel": "视觉主题:",
|
||||||
|
"template.settingsModal.themeOptions.default": "默认",
|
||||||
|
"template.settingsModal.themeOptions.sciFi": "科幻 (合成波)",
|
||||||
|
"template.settingsModal.themeOptions.fantasy": "奇幻 (古朴羊皮纸)",
|
||||||
|
"template.settingsModal.themeOptions.cyberpunk": "赛博朋克 (霓虹网格)",
|
||||||
|
"template.settingsModal.themeOptions.custom": "自定义",
|
||||||
|
"template.settingsModal.themeOptions.custom.background": "背景:",
|
||||||
|
"template.settingsModal.themeOptions.custom.accent": "强调色:",
|
||||||
|
"template.settingsModal.themeOptions.custom.text": "文字:",
|
||||||
|
"template.settingsModal.themeOptions.custom.highlight": "高亮:",
|
||||||
|
"template.settingsModal.theme.statBarLow": "状态条颜色 (低):",
|
||||||
|
"template.settingsModal.theme.statBarLowNote": "数值为 0% 时的颜色。",
|
||||||
|
"template.settingsModal.theme.statBarHigh": "状态条颜色 (高):",
|
||||||
|
"template.settingsModal.theme.statBarHighNote": "数值为 100% 时的颜色。",
|
||||||
|
"template.settingsModal.displayTitle": "显示选项",
|
||||||
|
"template.settingsModal.displayNote": "您可以在 SillyTavern 的扩展标签页中启用/禁用整个 RPG Companion 扩展。",
|
||||||
|
"template.settingsModal.display.panelPosition": "面板位置:",
|
||||||
|
"template.settingsModal.display.panelPositionOptions.right": "右侧边栏",
|
||||||
|
"template.settingsModal.display.panelPositionOptions.left": "左侧边栏",
|
||||||
|
"template.settingsModal.display.toggleAutoUpdate": "消息后自动更新",
|
||||||
|
"template.settingsModal.display.toggleAutoUpdateNote": "每条消息后自动刷新 RPG 信息。",
|
||||||
|
"template.settingsModal.display.showUserStats": "显示用户数值",
|
||||||
|
"template.settingsModal.display.showUserStatsNote": "启用用户数值,跟踪您角色的数值、心情、属性、技能等。",
|
||||||
|
"template.settingsModal.display.showInfoBox": "显示信息框",
|
||||||
|
"template.settingsModal.display.showInfoBoxNote": "显示位置、时间、天气和最近事件。",
|
||||||
|
"template.settingsModal.display.showPresentCharacters": "显示在场角色",
|
||||||
|
"template.settingsModal.display.showPresentCharactersNote": "显示角色肖像及其当前想法和状态。",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharacters": "显示聊天下方的在场角色",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharactersNote": "在聊天下方显示紧凑的在场角色面板。",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressions": "基于想法的表情",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressionsNote": "使用 SillyTavern Character Expressions 对聊天下方面板中每个在场角色的想法进行分类。Token 用量可能会因所选的分类 API 而增加。",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplay": "隐藏默认表情显示",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "隐藏 SillyTavern 内置的角色表情显示。",
|
||||||
|
"template.settingsModal.display.narratorMode": "旁白模式",
|
||||||
|
"template.settingsModal.display.narratorModeNote": "使用角色卡作为旁白。根据上下文推断角色,而非使用固定的角色引用。",
|
||||||
|
"template.settingsModal.display.showInventory": "显示物品栏",
|
||||||
|
"template.settingsModal.display.showInventoryNote": "跟踪携带的物品、穿戴的衣物、存储的物品和资产。",
|
||||||
|
"template.settingsModal.display.showQuests": "显示任务",
|
||||||
|
"template.settingsModal.display.showQuestsNote": "管理带有目标的主要和可选任务。",
|
||||||
|
"template.settingsModal.display.showLockIcons": "显示锁定/解锁跟踪器",
|
||||||
|
"template.settingsModal.display.showLockIconsNote": "在跟踪器项目上显示锁定/解锁图标,以防止 AI 修改它们。",
|
||||||
|
"template.settingsModal.display.showThoughtsInChat": "显示想法",
|
||||||
|
"template.settingsModal.display.showThoughtsInChatNote": "将角色想法显示为其消息旁边的气泡。",
|
||||||
|
"template.settingsModal.display.showInlineThoughts": "在消息文本下方显示想法",
|
||||||
|
"template.settingsModal.display.showInlineThoughtsNote": "在默认角落想法气泡和显示在消息文本下方的想法卡片之间切换。",
|
||||||
|
"template.settingsModal.display.alwaysShowThoughtBubble": "始终显示想法气泡",
|
||||||
|
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "自动展开想法气泡,无需先点击图标",
|
||||||
|
"template.settingsModal.display.enableAnimations": "启用动画",
|
||||||
|
"template.settingsModal.display.enableAnimationsNote": "数值、内容更新和掷骰的平滑过渡。",
|
||||||
|
"template.settingsModal.display.showImmersiveHtmlToggle": "显示沉浸式 HTML",
|
||||||
|
"template.settingsModal.display.showImmersiveHtmlToggleNote": "显示一个切换按钮以启用/禁用消息中的 HTML 格式。",
|
||||||
|
"template.settingsModal.display.showDialogueColoringToggle": "显示彩色对话",
|
||||||
|
"template.settingsModal.display.showDialogueColoringToggleNote": "显示一个切换按钮以启用/禁用彩色对话格式。",
|
||||||
|
"template.settingsModal.display.showDeceptionToggle": "显示欺骗系统",
|
||||||
|
"template.settingsModal.display.showDeceptionToggleNote": "显示一个切换按钮以启用/禁用用于标记谎言和欺骗的欺骗系统。",
|
||||||
|
"template.settingsModal.display.showOmniscienceToggle": "显示全知过滤器",
|
||||||
|
"template.settingsModal.display.showOmniscienceToggleNote": "显示一个切换按钮以启用/禁用用于过滤隐藏事件的全知过滤器。",
|
||||||
|
"template.settingsModal.display.showSpotifyMusicToggle": "显示 Spotify 音乐",
|
||||||
|
"template.settingsModal.display.showSpotifyMusicToggleNote": "显示 Spotify 音乐播放器,带有 AI 推荐的适合场景的曲目。",
|
||||||
|
"template.settingsModal.display.showSnowflakesToggle": "显示雪花效果",
|
||||||
|
"template.settingsModal.display.showDynamicWeatherToggle": "显示动态天气效果",
|
||||||
|
"template.settingsModal.display.showDynamicWeatherToggleNote": "显示一个切换按钮以启用/禁用动画天气效果。",
|
||||||
|
"template.settingsModal.display.showNarratorMode": "显示旁白模式",
|
||||||
|
"template.settingsModal.display.showNarratorModeNote": "显示一个切换按钮以启用/禁用旁白模式(根据上下文推断角色)。",
|
||||||
|
"template.settingsModal.display.showAutoAvatars": "显示自动生成头像",
|
||||||
|
"template.settingsModal.display.showAutoAvatarsNote": "显示一个切换按钮以自动为没有图片的角色生成头像。",
|
||||||
|
"template.settingsModal.display.showRandomizedPlot": "显示随机化剧情推进",
|
||||||
|
"template.settingsModal.display.showRandomizedPlotNote": "显示用于 AI 生成的随机剧情推进提示的按钮。",
|
||||||
|
"template.settingsModal.display.showNaturalPlot": "显示自然剧情推进",
|
||||||
|
"template.settingsModal.display.showNaturalPlotNote": "显示用于上下文感知的叙事延续提示的按钮。",
|
||||||
|
"template.settingsModal.display.showStartEncounter": "显示开始遭遇",
|
||||||
|
"template.settingsModal.display.showStartEncounterNote": "显示按钮以启动交互式战斗遭遇。",
|
||||||
|
"template.settingsModal.display.showDiceDisplay": "显示掷骰显示",
|
||||||
|
"template.settingsModal.display.showDiceDisplayNote": "在面板中显示“上次掷骰”指示器。",
|
||||||
|
"template.settingsModal.display.showCYOAToggle": "显示选择冒险",
|
||||||
|
"template.settingsModal.display.showCYOAToggleNote": "显示一个切换按钮,用于启用/禁用“选择你自己的冒险”格式指令,该指令使模型在输出结束时生成五个可能的行动/对话供你选择。",
|
||||||
|
"template.settingsModal.display.weatherPosition.background": "在背景中显示",
|
||||||
|
"template.settingsModal.display.weatherPosition.backgroundNote": "在聊天背景中显示天气效果(标准行为)。",
|
||||||
|
"template.settingsModal.display.weatherPosition.foreground": "在前景中显示",
|
||||||
|
"template.settingsModal.display.weatherPosition.foregroundNote": "在聊天前景中显示天气效果(实验性)。",
|
||||||
|
"template.mainPanel.autoAvatars": "自动头像",
|
||||||
|
"template.settingsModal.advancedTitle": "高级",
|
||||||
|
"template.settingsModal.advanced.encounterHistoryDepth": "遭遇战的聊天历史深度:",
|
||||||
|
"template.settingsModal.advanced.encounterHistoryDepthNote": "包含在战斗初始化中的最近消息数量。",
|
||||||
|
"template.settingsModal.advanced.autoSaveCombatLogs": "自动保存战斗日志",
|
||||||
|
"template.settingsModal.advanced.autoSaveCombatLogsNote": "将详细战斗日志保存到文件以供将来参考和分析。",
|
||||||
|
"template.settingsModal.advanced.clearCacheNote": "清除当前活动聊天中已提交和显示的跟踪器数据。",
|
||||||
|
"template.settingsModal.advanced.generationMode": "生成模式:",
|
||||||
|
"template.settingsModal.advanced.generationModeOptions.together": "集成生成",
|
||||||
|
"template.settingsModal.advanced.generationModeOptions.separate": "单独生成",
|
||||||
|
"template.settingsModal.advanced.generationModeNote": "集成:将 RPG 跟踪添加到主要角色扮演中。单独:单独生成 RPG 数据(手动或自动)。外部:直接连接到 OpenAI 兼容端点。",
|
||||||
|
"template.settingsModal.advanced.generationModeOptions.external": "外部 API",
|
||||||
|
"template.settingsModal.advanced.externalApi.title": "外部 API 设置",
|
||||||
|
"template.settingsModal.advanced.externalApi.baseUrl": "API 基础 URL",
|
||||||
|
"template.settingsModal.advanced.externalApi.baseUrlNote": "OpenAI 兼容端点(例如 OpenAI、OpenRouter、本地 LLM 服务器)。",
|
||||||
|
"template.settingsModal.advanced.externalApi.apiKey": "API 密钥",
|
||||||
|
"template.settingsModal.advanced.externalApi.apiKeyNote": "您的外部服务 API 密钥。",
|
||||||
|
"template.settingsModal.advanced.externalApi.model": "模型",
|
||||||
|
"template.settingsModal.advanced.externalApi.modelNote": "模型标识符(例如 gpt-4o-mini、claude-3-haiku、mistral-7b)。",
|
||||||
|
"template.settingsModal.advanced.externalApi.maxTokens": "最大token数",
|
||||||
|
"template.settingsModal.advanced.externalApi.temperature": "温度",
|
||||||
|
"template.settingsModal.advanced.externalApi.testConnection": "测试连接",
|
||||||
|
"template.settingsModal.advanced.contextMessages": "上下文消息:",
|
||||||
|
"template.settingsModal.advanced.contextMessagesNote": "包含的最近消息数量。",
|
||||||
|
"template.settingsModal.advanced.useSeparatePreset": "使用连接到 RPG Companion Trackers 预设的模型",
|
||||||
|
"template.settingsModal.advanced.useSeparatePresetNote": "启用后,跟踪器生成将使用“RPG Companion Trackers”预设中的模型,而不是您的主 API 模型。预设将在生成期间自动切换并在之后恢复。在该预设中选择所需模型,并确保“将预设绑定到 API 连接”切换已打开(位于导入/导出预设按钮旁边)。",
|
||||||
|
"template.settingsModal.advanced.skipInjections": "在引导生成期间跳过注入:",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsOptions.none": "从不跳过",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsOptions.impersonation": "仅在模拟请求时",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsOptions.guided": "始终针对引导或静默提示",
|
||||||
|
"template.settingsModal.advanced.skipInjectionsNote": "设置后,当检测到引导生成(通过 `instruct` 或 `quiet_prompt`)时,扩展将不会根据所选模式注入跟踪器提示、示例或 HTML 指令。在使用 GuidedGenerations 或类似扩展时很有用。",
|
||||||
|
"template.settingsModal.advanced.customHtmlPromptTitle": "自定义 HTML 提示:",
|
||||||
|
"template.settingsModal.advanced.restoreDefaultHtmlPrompt": "恢复默认",
|
||||||
|
"template.settingsModal.advanced.customHtmlPromptNote": "自定义启用“启用沉浸式 HTML”时注入的 HTML 提示。默认提示显示在上面 - 您可以直接编辑或完全替换。点击“恢复默认”以重置。这会影响所有生成模式(同时、单独和剧情推进)。",
|
||||||
|
"template.settingsModal.advanced.clearCache": "清除扩展缓存",
|
||||||
|
"template.settingsModal.advanced.resetFabPositions": "重置按钮位置",
|
||||||
|
"template.settingsModal.advanced.resetFabPositionsNote": "将所有浮动操作按钮(切换、刷新、调试)重置为默认的左上角位置。如果按钮在屏幕外,这很有用。",
|
||||||
|
"template.trackerEditorModal.title": "编辑跟踪器",
|
||||||
|
"template.trackerEditorModal.tabs.userStats": "用户数值",
|
||||||
|
"template.trackerEditorModal.tabs.infoBox": "信息框",
|
||||||
|
"template.trackerEditorModal.tabs.presentCharacters": "在场角色",
|
||||||
|
"template.trackerEditorModal.buttons.reset": "重置",
|
||||||
|
"template.trackerEditorModal.buttons.cancel": "取消",
|
||||||
|
"template.trackerEditorModal.buttons.save": "保存并应用",
|
||||||
|
"template.trackerEditorModal.buttons.export": "导出",
|
||||||
|
"template.trackerEditorModal.buttons.import": "导入",
|
||||||
|
"template.trackerEditorModal.messages.exportSuccess": "跟踪器预设导出成功!",
|
||||||
|
"template.trackerEditorModal.messages.exportError": "跟踪器预设导出失败。请检查控制台以获取详细信息。",
|
||||||
|
"template.trackerEditorModal.messages.importSuccess": "跟踪器预设导入成功!",
|
||||||
|
"template.trackerEditorModal.messages.importError": "跟踪器预设导入失败",
|
||||||
|
"template.trackerEditorModal.messages.importConfirm": "这将替换您当前的跟踪器配置。继续吗?",
|
||||||
|
"template.trackerEditorModal.userStatsTab.customStatsTitle": "自定义数值",
|
||||||
|
"template.trackerEditorModal.userStatsTab.addCustomStatButton": "添加自定义数值",
|
||||||
|
"template.trackerEditorModal.userStatsTab.rpgAttributesTitle": "RPG 属性",
|
||||||
|
"template.trackerEditorModal.userStatsTab.enableRpgAttributes": "启用 RPG 属性部分",
|
||||||
|
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributes": "始终在提示中包含属性",
|
||||||
|
"template.trackerEditorModal.userStatsTab.alwaysIncludeAttributesNote": "如果禁用,属性仅在掷骰活动时发送。",
|
||||||
|
"template.trackerEditorModal.userStatsTab.addAttributeButton": "添加属性",
|
||||||
|
"template.trackerEditorModal.userStatsTab.statusSectionTitle": "状态部分",
|
||||||
|
"template.trackerEditorModal.userStatsTab.enableStatusSection": "启用状态部分",
|
||||||
|
"template.trackerEditorModal.userStatsTab.showMoodEmoji": "显示心情表情",
|
||||||
|
"template.trackerEditorModal.userStatsTab.statusFieldsLabel": "状态字段(逗号分隔):",
|
||||||
|
"template.trackerEditorModal.userStatsTab.skillsSectionTitle": "技能部分",
|
||||||
|
"template.trackerEditorModal.userStatsTab.enableSkillsSection": "启用技能部分",
|
||||||
|
"template.trackerEditorModal.userStatsTab.skillsLabelLabel": "技能标签:",
|
||||||
|
"template.trackerEditorModal.userStatsTab.skillsListLabel": "技能列表(逗号分隔):",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.widgetsTitle": "小部件",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.dateWidget": "日期",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.weatherWidget": "天气",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.temperatureWidget": "温度",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.timeWidget": "时间",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.locationWidget": "位置",
|
||||||
|
"template.trackerEditorModal.infoBoxTab.recentEventsWidget": "最近事件",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.relationshipStatusTitle": "关系状态字段",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.enableRelationshipStatus": "启用关系状态字段",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.relationshipStatusHint": "定义关系类型,并在角色肖像上显示相应的表情符号。",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.newRelationshipButton": "新关系",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorTitle": "外观/举止字段",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.appearanceDemeanorHint": "显示在角色名字下方的字段。",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.addCustomFieldButton": "添加自定义字段",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.thoughtsConfigTitle": "想法配置",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.enableCharacterThoughts": "启用角色想法",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.thoughtsLabelLabel": "想法标签:",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.aiInstructionLabel": "AI 指令:",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.characterStatsTitle": "角色数值",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.trackCharacterStats": "跟踪角色数值",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.characterStatsHint": "为每个角色创建要跟踪的数值(显示为彩色数字)。",
|
||||||
|
"template.trackerEditorModal.presentCharactersTab.addCharacterStatButton": "添加角色数值",
|
||||||
|
"template.mainPanel.title": "RPG Companion",
|
||||||
|
"template.mainPanel.lastRoll": "上次掷骰:",
|
||||||
|
"template.mainPanel.clearLastRoll": "清除上次掷骰",
|
||||||
|
"template.mainPanel.immersiveHtml": "沉浸式 HTML",
|
||||||
|
"template.mainPanel.coloredDialogues": "彩色对话",
|
||||||
|
"template.mainPanel.deceptionSystem": "欺骗系统",
|
||||||
|
"template.mainPanel.omniscienceFilter": "全知过滤器",
|
||||||
|
"template.mainPanel.cyoa": "选择冒险",
|
||||||
|
"template.mainPanel.spotifyMusic": "Spotify 音乐",
|
||||||
|
"template.mainPanel.snowflakesEffect": "雪花效果",
|
||||||
|
"template.mainPanel.dynamicWeatherEffects": "动态天气",
|
||||||
|
"template.mainPanel.narratorMode": "旁白模式",
|
||||||
|
"template.mainPanel.refreshRpgInfo": "刷新 RPG 信息",
|
||||||
|
"template.mainPanel.updating": "更新中...",
|
||||||
|
"template.mainPanel.editTrackersButton": "编辑跟踪器",
|
||||||
|
"template.mainPanel.settingsButton": "设置",
|
||||||
|
"global.none": "无",
|
||||||
|
"global.add": "添加",
|
||||||
|
"global.cancel": "取消",
|
||||||
|
"global.listView": "列表视图",
|
||||||
|
"global.gridView": "网格视图",
|
||||||
|
"global.save": "保存",
|
||||||
|
"global.status": "状态",
|
||||||
|
"global.inventory": "物品栏",
|
||||||
|
"global.quests": "任务",
|
||||||
|
"global.info": "信息",
|
||||||
|
"global.removeItem": "移除物品",
|
||||||
|
"global.clickToEdit": "点击编辑",
|
||||||
|
"global.collapseExpandPanel": "折叠/展开面板",
|
||||||
|
"global.refreshRpgInfo": "刷新RPG信息",
|
||||||
|
"global.showHideApiKey": "显示/隐藏API密钥",
|
||||||
|
"global.closeDialog": "关闭对话框",
|
||||||
|
"infobox.noData.title": "尚无数据",
|
||||||
|
"infobox.noData.instruction": "在角色扮演中生成新响应,或在设置中切换到“单独生成”以访问并点击“刷新 RPG 信息”按钮",
|
||||||
|
"infobox.recentEvents.title": "最近事件",
|
||||||
|
"infobox.recentEvents.addEventPlaceholder": "添加事件...",
|
||||||
|
"inventory.section.onPerson": "随身携带",
|
||||||
|
"inventory.section.clothing": "衣物",
|
||||||
|
"inventory.section.stored": "存储",
|
||||||
|
"inventory.section.assets": "资产",
|
||||||
|
"inventory.onPerson.empty": "未携带任何物品",
|
||||||
|
"inventory.onPerson.title": "当前携带的物品",
|
||||||
|
"inventory.onPerson.addItemButton": "添加物品",
|
||||||
|
"inventory.onPerson.addItemPlaceholder": "输入物品名称...",
|
||||||
|
"inventory.clothing.empty": "未穿戴任何衣物",
|
||||||
|
"inventory.clothing.title": "衣物和护甲",
|
||||||
|
"inventory.clothing.addItemButton": "添加衣物",
|
||||||
|
"inventory.clothing.addItemPlaceholder": "输入衣物物品...",
|
||||||
|
"inventory.stored.title": "存储位置",
|
||||||
|
"inventory.stored.addLocationButton": "添加位置",
|
||||||
|
"inventory.stored.addLocationPlaceholder": "输入位置名称...",
|
||||||
|
"inventory.stored.saveButton": "保存",
|
||||||
|
"inventory.stored.empty": "尚无存储位置。点击“添加位置”以创建一个。",
|
||||||
|
"inventory.stored.noItems": "此处未存储任何物品",
|
||||||
|
"inventory.stored.addItemToLocationPlaceholder": "输入物品名称...",
|
||||||
|
"inventory.stored.addItemButton": "添加物品",
|
||||||
|
"inventory.stored.confirmRemoveLocationMessage": "删除“${location}”?这将删除存储在该处的所有物品。",
|
||||||
|
"inventory.stored.confirmRemoveLocationConfirmButton": "确认",
|
||||||
|
"inventory.assets.empty": "未拥有任何资产",
|
||||||
|
"inventory.assets.title": "车辆、财产和主要所有物",
|
||||||
|
"inventory.assets.addAssetModalTitle": "添加资产",
|
||||||
|
"inventory.assets.addAssetButton": "添加资产",
|
||||||
|
"inventory.assets.addAssetPlaceholder": "输入资产名称...",
|
||||||
|
"inventory.assets.description": "资产包括车辆(汽车、摩托车)、财产(房屋、公寓)和主要设备(车间工具、特殊物品)。",
|
||||||
|
"inventory.onPerson.addItemTitle": "添加新物品",
|
||||||
|
"inventory.clothing.addItemTitle": "添加新衣物",
|
||||||
|
"inventory.stored.addLocationTitle": "添加新存储位置",
|
||||||
|
"inventory.stored.addItemToLocationTitle": "添加物品到此位置",
|
||||||
|
"inventory.stored.removeLocationTitle": "移除此存储位置",
|
||||||
|
"inventory.assets.addItemTitle": "添加新资产",
|
||||||
|
"inventory.assets.removeAssetTitle": "移除资产",
|
||||||
|
"quests.section.main": "主要任务",
|
||||||
|
"quests.section.optional": "可选任务",
|
||||||
|
"quests.main.title": "主要任务",
|
||||||
|
"quests.main.addQuestButton": "添加任务",
|
||||||
|
"quests.main.addQuestPlaceholder": "输入主要任务标题...",
|
||||||
|
"quests.main.empty": "无活跃的主要任务",
|
||||||
|
"quests.main.hint": "主要任务代表您在故事中的主要目标。",
|
||||||
|
"quests.optional.title": "可选任务",
|
||||||
|
"quests.optional.addQuestButton": "添加任务",
|
||||||
|
"quests.optional.addQuestPlaceholder": "输入可选任务标题...",
|
||||||
|
"quests.optional.empty": "无活跃的可选任务",
|
||||||
|
"quests.optional.hint": "可选任务是补充您主要故事的次要目标。",
|
||||||
|
"quests.editQuestTitle": "编辑任务",
|
||||||
|
"quests.removeQuestTitle": "完成/移除任务",
|
||||||
|
"checkpoint.setChapterStart": "设置章节开始",
|
||||||
|
"checkpoint.clearChapterStart": "清除章节开始",
|
||||||
|
"checkpoint.indicator": "章节开始",
|
||||||
|
"checkpoint.tooltip": "此点之前的消息从上下文中排除",
|
||||||
|
"musicPlayer.title": "场景音乐",
|
||||||
|
"musicPlayer.noMusic": "AI 将在适合场景时推荐音乐",
|
||||||
|
"errors.parsingError": "RPG Companion Trackers 解析错误!模型返回了错误的格式。如果问题持续存在,请考虑更换生成模型。",
|
||||||
|
"settings.recommendedModels.title": "推荐模型",
|
||||||
|
"settings.recommendedModels.description": "为使扩展正常工作,**不建议使用任何低于 20B 的模型,尤其是旧模型。** 它与 SOTA 模型(如 Deepseek、Claude、GPT 或 Gemini)配合最佳。",
|
||||||
|
"thoughts.addCharacter": "添加角色",
|
||||||
|
"thoughts.locked": "已锁定",
|
||||||
|
"thoughts.unlocked": "已解锁",
|
||||||
|
"thoughts.clickToEdit": "点击编辑",
|
||||||
|
"thoughts.clickToUpload": "点击上传头像",
|
||||||
|
"thoughts.removeCharacter": "移除角色",
|
||||||
|
"thoughts.empty": "尚无角色数据生成",
|
||||||
|
"userStats.level": "LVL",
|
||||||
|
"userStats.clickToEditLevel": "点击编辑等级",
|
||||||
|
"userStats.statsLocked": "已锁定 - AI 无法更改数值",
|
||||||
|
"userStats.statsUnlocked": "已解锁 - AI 可以更改数值",
|
||||||
|
"userStats.clickToEditStatName": "点击编辑数值名称",
|
||||||
|
"userStats.clickToEditStatValue": "点击编辑",
|
||||||
|
"userStats.moodLocked": "已锁定 - AI 无法更改心情",
|
||||||
|
"userStats.moodUnlocked": "已解锁 - AI 可以更改心情",
|
||||||
|
"userStats.clickToEditEmoji": "点击编辑表情",
|
||||||
|
"userStats.skillsLocked": "已锁定 - AI 无法更改技能",
|
||||||
|
"userStats.skillsUnlocked": "已解锁 - AI 可以更改技能",
|
||||||
|
"userStats.clickToEditSkills": "点击编辑技能",
|
||||||
|
"userStats.empty": "尚无数值生成",
|
||||||
|
"infoBox.clickToEdit": "点击编辑",
|
||||||
|
"infoBox.locked": "已锁定 - AI 无法更改此项",
|
||||||
|
"infoBox.unlocked": "已解锁 - AI 可以更改此项",
|
||||||
|
"infoBox.weatherFallback": "天气",
|
||||||
|
"infoBox.locationFallback": "位置",
|
||||||
|
"stats.health": "Health",
|
||||||
|
"stats.satiety": "Satiety",
|
||||||
|
"stats.energy": "Energy",
|
||||||
|
"stats.hygiene": "Hygiene",
|
||||||
|
"stats.arousal": "Arousal",
|
||||||
|
"stats.str": "STR",
|
||||||
|
"stats.dex": "DEX",
|
||||||
|
"stats.con": "CON",
|
||||||
|
"stats.int": "INT",
|
||||||
|
"stats.wis": "WIS",
|
||||||
|
"stats.cha": "CHA",
|
||||||
|
"stats.displayMode": "显示模式:",
|
||||||
|
"stats.displayMode.percentage": "百分比",
|
||||||
|
"stats.displayMode.number": "数值",
|
||||||
|
"dice.title": "掷骰子",
|
||||||
|
"dice.numberOfDice": "骰子数量:",
|
||||||
|
"dice.diceType": "骰子类型:",
|
||||||
|
"dice.rolling": "掷骰中...",
|
||||||
|
"dice.result": "结果:",
|
||||||
|
"dice.saveRoll": "保存掷骰",
|
||||||
|
"preset.createNewPresetTitle": "创建新预设",
|
||||||
|
"preset.deleteCurrentPresetTitle": "删除当前预设",
|
||||||
|
"preset.setDefaultPresetTitle": "设为默认预设",
|
||||||
|
"preset.defaultPresetDescription": "这是默认预设",
|
||||||
|
"preset.label": "预设:",
|
||||||
|
"preset.useThisPresetFor": "将此预设用于:",
|
||||||
|
"stats.showLevel": "显示等级",
|
||||||
|
"dateFormat.weekdayMonthYear": "星期,月份,年份",
|
||||||
|
"dateFormat.dayNumericalMonthYear": "日(数字),月份,年份",
|
||||||
|
"historyPersistence.tabTitle": "历史持久性",
|
||||||
|
"historyPersistence.settingsTitle": "历史持久性设置",
|
||||||
|
"historyPersistence.enable": "启用历史持久性",
|
||||||
|
"template.trackerEditorModal.tabs.historyPersistence": "历史持久性",
|
||||||
|
"historyPersistence.hint": "将选定的跟踪器数据注入历史消息中,帮助AI保持时间敏感事件、天气变化和位置跟踪的连续性。",
|
||||||
|
"historyPersistence.sendAllEnabledStats": "刷新时发送所有启用的数值",
|
||||||
|
"historyPersistence.sendAllEnabledStatsHint": "启用后,刷新RPG信息将在历史上下文中包含预设中的所有启用数值,忽略下面的单个选择。",
|
||||||
|
"historyPersistence.numberOfMessages": "包含的消息数量(0 = 所有可用):",
|
||||||
|
"historyPersistence.injectionPosition": "注入位置:",
|
||||||
|
"historyPersistence.injectionPosition.userMessageEnd": "用户消息末尾",
|
||||||
|
"historyPersistence.injectionPosition.assistantMessageEnd": "助手消息末尾",
|
||||||
|
"historyPersistence.customContextPreamble": "自定义上下文前言:",
|
||||||
|
"historyPersistence.customContextPreamblePlaceholder": "该时刻的上下文:",
|
||||||
|
"historyPersistence.userStatsSection": "用户数值",
|
||||||
|
"historyPersistence.userStatsHint": "选择哪些数值应包含在历史消息中。",
|
||||||
|
"historyPersistence.statusSection": "状态(心情/状况)",
|
||||||
|
"historyPersistence.inventory": "物品栏",
|
||||||
|
"historyPersistence.quests": "任务",
|
||||||
|
"historyPersistence.infoBoxSection": "信息框",
|
||||||
|
"historyPersistence.infoBoxHint": "选择哪些信息框字段应包含在历史消息中。这些字段推荐用于时间跟踪。",
|
||||||
|
"historyPersistence.presentCharactersSection": "当前角色",
|
||||||
|
"historyPersistence.presentCharactersHint": "选择哪些角色字段应包含在历史消息中。",
|
||||||
|
"historyPersistence.widget.date": "日期",
|
||||||
|
"historyPersistence.widget.weather": "天气",
|
||||||
|
"historyPersistence.widget.temperature": "温度",
|
||||||
|
"historyPersistence.widget.time": "时间",
|
||||||
|
"historyPersistence.widget.location": "位置",
|
||||||
|
"historyPersistence.widget.recentEvents": "近期事件",
|
||||||
|
"historyPersistence.thoughts": "想法",
|
||||||
|
"historyPersistence.skills": "技能",
|
||||||
|
"template.promptsEditor.button": "自定义提示",
|
||||||
|
"template.promptsEditor.buttonNote": "编辑用于生成、剧情推进和战斗遭遇的所有AI提示。",
|
||||||
|
"template.promptsEditor.title": "自定义提示",
|
||||||
|
"template.promptsEditor.description": "自定义整个扩展中使用的AI提示。留空字段以使用默认值。",
|
||||||
|
"template.promptsEditor.restoreDefault": "恢复默认",
|
||||||
|
"template.promptsEditor.htmlPrompt.title": "HTML提示",
|
||||||
|
"template.promptsEditor.htmlPrompt.note": "当“启用沉浸式HTML”启用时注入。影响所有生成模式。",
|
||||||
|
"template.promptsEditor.dialogueColoringPrompt.title": "对话着色提示",
|
||||||
|
"template.promptsEditor.dialogueColoringPrompt.note": "当“启用彩色对话”启用时注入。影响所有生成模式。",
|
||||||
|
"template.promptsEditor.deceptionPrompt.title": "欺骗系统提示",
|
||||||
|
"template.promptsEditor.deceptionPrompt.note": "当“启用欺骗系统”启用时注入。指示AI用隐藏标签标记谎言和欺骗行为。",
|
||||||
|
"template.promptsEditor.omnisciencePrompt.title": "全知过滤器提示",
|
||||||
|
"template.promptsEditor.omnisciencePrompt.note": "当“启用全知过滤器”启用时注入。指示AI将玩家角色无法感知的信息分离到隐藏的过滤器标签中。",
|
||||||
|
"template.promptsEditor.cyoaPrompt.title": "选择冒险提示",
|
||||||
|
"template.promptsEditor.cyoaPrompt.note": "当“启用选择冒险”启用时注入。指示AI在回复结尾提供带编号的动作选项。使用非常高的优先级(深度102)确保它是最后一条指令。",
|
||||||
|
"template.promptsEditor.spotifyPrompt.title": "Spotify音乐提示",
|
||||||
|
"template.promptsEditor.spotifyPrompt.note": "当“启用Spotify音乐”启用时注入。要求AI为场景推荐合适的音乐。",
|
||||||
|
"template.promptsEditor.narratorPrompt.title": "旁白模式提示",
|
||||||
|
"template.promptsEditor.narratorPrompt.note": "当“旁白模式”启用时注入。指示AI从上下文中推断角色信息。",
|
||||||
|
"template.promptsEditor.contextPrompt.title": "上下文指令提示",
|
||||||
|
"template.promptsEditor.contextPrompt.note": "在Separate/External模式中,上下文摘要后注入。告诉AI如何使用上下文。",
|
||||||
|
"template.promptsEditor.randomPlotPrompt.title": "随机剧情推进提示",
|
||||||
|
"template.promptsEditor.randomPlotPrompt.note": "当点击“随机剧情”按钮时注入。为故事引入随机元素。",
|
||||||
|
"template.promptsEditor.naturalPlotPrompt.title": "自然剧情推进提示",
|
||||||
|
"template.promptsEditor.naturalPlotPrompt.note": "当点击“自然剧情”按钮时注入。自然地推进故事发展。",
|
||||||
|
"template.promptsEditor.avatarPrompt.title": "头像生成指令",
|
||||||
|
"template.promptsEditor.avatarPrompt.note": "生成头像图像提示时给LLM的指令。用于“自动生成缺失头像”功能。",
|
||||||
|
"template.promptsEditor.trackerPrompt.title": "跟踪器指令",
|
||||||
|
"template.promptsEditor.trackerPrompt.note": "仅指令部分(格式规范已硬编码)。{userName}将被替换为用户名称。",
|
||||||
|
"template.promptsEditor.trackerContinuationPrompt.title": "跟踪器延续指令",
|
||||||
|
"template.promptsEditor.trackerContinuationPrompt.note": "在跟踪器格式规范后添加的指令,告诉AI如何继续叙事。",
|
||||||
|
"template.promptsEditor.combatPrompt.title": "战斗叙事风格指令",
|
||||||
|
"template.promptsEditor.combatPrompt.note": "战斗遭遇的写作风格指令。包括散文质量指南和防重复规则。{userName}将被替换为用户名称。",
|
||||||
|
"template.settingsModal.mobileFabTitle": "移动按钮小部件",
|
||||||
|
"template.settingsModal.mobileFabNote": "在移动设备上显示围绕浮动按钮的紧凑信息小部件。小部件自动定位。",
|
||||||
|
"template.settingsModal.mobileFab.enabled": "启用浮动移动小部件",
|
||||||
|
"template.settingsModal.mobileFab.enabledNote": "主开关,用于在移动浮动按钮周围显示信息小部件。",
|
||||||
|
"template.settingsModal.mobileFab.weatherIcon": "天气图标",
|
||||||
|
"template.settingsModal.mobileFab.weatherDesc": "天气描述",
|
||||||
|
"template.settingsModal.mobileFab.clock": "时间/时钟",
|
||||||
|
"template.settingsModal.mobileFab.date": "日期",
|
||||||
|
"template.settingsModal.mobileFab.location": "位置",
|
||||||
|
"template.settingsModal.mobileFab.stats": "数值(生命值、能量等)",
|
||||||
|
"template.settingsModal.mobileFab.attributes": "RPG属性(力量、敏捷等)",
|
||||||
|
"template.settingsModal.desktopStripTitle": "桌面折叠面板条小部件",
|
||||||
|
"template.settingsModal.desktopStripNote": "在桌面上的折叠面板条中显示紧凑信息小部件。垂直显示数值,无需展开面板。",
|
||||||
|
"template.settingsModal.desktopStrip.enabled": "启用面板条小部件",
|
||||||
|
"template.settingsModal.desktopStrip.enabledNote": "在折叠面板条中显示小部件,以便快速访问数值。",
|
||||||
|
"template.settingsModal.desktopStrip.weatherIcon": "天气图标",
|
||||||
|
"template.settingsModal.desktopStrip.clock": "时间/时钟",
|
||||||
|
"template.settingsModal.desktopStrip.date": "日期",
|
||||||
|
"template.settingsModal.desktopStrip.location": "位置",
|
||||||
|
"template.settingsModal.desktopStrip.stats": "数值(生命值、能量等)",
|
||||||
|
"template.settingsModal.desktopStrip.attributes": "RPG属性(力量、敏捷等)",
|
||||||
|
"plotProgression.buttons.randomizedPlot": "随机化剧情",
|
||||||
|
"plotProgression.buttons.naturalPlot": "自然剧情",
|
||||||
|
"plotProgression.buttons.enterEncounter": "进入遭遇战",
|
||||||
|
"plotProgression.tooltips.randomizedPlot": "生成随机剧情转折或事件",
|
||||||
|
"plotProgression.tooltips.naturalPlot": "无转折地自然延续故事",
|
||||||
|
"plotProgression.tooltips.enterEncounter": "进入战斗遭遇",
|
||||||
|
"encounter.configModal.title": "配置战斗叙事",
|
||||||
|
"encounter.configModal.combatNarrativeStyle": "战斗叙事风格",
|
||||||
|
"encounter.configModal.combatSummaryStyle": "战斗总结风格",
|
||||||
|
"encounter.configModal.labels.tense": "时态:",
|
||||||
|
"encounter.configModal.labels.person": "人称:",
|
||||||
|
"encounter.configModal.labels.narration": "叙述:",
|
||||||
|
"encounter.configModal.labels.pointOfView": "视角:",
|
||||||
|
"encounter.configModal.options.present": "现在时",
|
||||||
|
"encounter.configModal.options.past": "过去时",
|
||||||
|
"encounter.configModal.options.firstPerson": "第一人称",
|
||||||
|
"encounter.configModal.options.secondPerson": "第二人称",
|
||||||
|
"encounter.configModal.options.thirdPerson": "第三人称",
|
||||||
|
"encounter.configModal.options.omniscient": "全知视角",
|
||||||
|
"encounter.configModal.options.limited": "有限视角",
|
||||||
|
"encounter.configModal.placeholders.narrator": "叙述者",
|
||||||
|
"encounter.configModal.rememberSettings": "记住这些设置以供未来遭遇使用",
|
||||||
|
"encounter.configModal.buttons.proceed": "继续",
|
||||||
|
"encounter.ui.concludeEncounterTitle": "提前结束遭遇",
|
||||||
|
"encounter.ui.closeTitle": "关闭(结束战斗)",
|
||||||
|
"encounter.ui.initializingCombat": "正在初始化战斗...",
|
||||||
|
"encounter.ui.initializingCombatEncounter": "正在初始化战斗遭遇...",
|
||||||
|
"encounter.ui.combatBegins": "战斗开始!",
|
||||||
|
"encounter.ui.allEnemies": "所有敌人",
|
||||||
|
"encounter.ui.areaOfEffect": "范围效果",
|
||||||
|
"encounter.ui.youHaveBeenDefeated": "你已被击败...",
|
||||||
|
"encounter.ui.attacks": "攻击",
|
||||||
|
"encounter.ui.items": "物品",
|
||||||
|
"encounter.ui.customAction": "自定义动作",
|
||||||
|
"encounter.ui.customActionPlaceholder": "描述你想要做什么...",
|
||||||
|
"encounter.ui.generatingCombatSummary": "正在生成战斗总结...",
|
||||||
|
"encounter.ui.pleaseWait": "请稍候...",
|
||||||
|
"encounter.ui.failedToCreateSummary": "无法创建总结。你可以关闭此窗口。",
|
||||||
|
"encounter.ui.wrongFormatDetected": "检测到错误格式",
|
||||||
|
"encounter.ui.concludeEncounterButton": "结束遭遇",
|
||||||
|
"encounter.ui.combatEncounterTitle": "战斗遭遇",
|
||||||
|
"encounter.ui.errorGeneratingCombatSummary": "生成战斗总结时出错。",
|
||||||
|
"encounter.ui.closeCombatWindow": "关闭战斗窗口",
|
||||||
|
"encounter.ui.combatLog": "战斗日志",
|
||||||
|
"encounter.ui.selectTarget": "选择目标",
|
||||||
|
"encounter.ui.submit": "提交",
|
||||||
|
"encounter.ui.regenerate": "重新生成",
|
||||||
|
"encounter.ui.or": "或",
|
||||||
|
"encounter.ui.result.victory": "胜利",
|
||||||
|
"encounter.ui.result.defeat": "失败",
|
||||||
|
"encounter.ui.result.fled": "逃跑",
|
||||||
|
"encounter.ui.result.interrupted": "中断",
|
||||||
|
"encounter.ui.error.noResponse": "未收到AI响应。模型可能不可用。",
|
||||||
|
"encounter.ui.error.invalidJsonFormat": "检测到无效的JSON格式。AI返回了格式错误的数据。请确保最大响应长度至少设置为2048个token,否则模型可能会用完token并产生不完整的结构。",
|
||||||
|
"encounter.ui.error.failedToInitialize": "初始化战斗失败:",
|
||||||
|
"encounter.ui.error.errorProcessingAction": "处理动作时出错:",
|
||||||
|
"encounter.ui.combatSummaryAddedBy": "战斗总结已由{speakerName}添加到聊天。",
|
||||||
|
"encounter.ui.combatSummaryAdded": "战斗总结已添加到聊天。",
|
||||||
|
"encounter.ui.environment.default": "战斗竞技场",
|
||||||
|
"encounter.ui.enemiesTitle": "敌人",
|
||||||
|
"encounter.ui.partyTitle": "队伍",
|
||||||
|
"encounter.ui.hpSuffix": " HP",
|
||||||
|
"encounter.ui.playerSuffix": "(你)",
|
||||||
|
"encounter.ui.confirmConcludeEarly": "提前结束遭遇战并生成总结?",
|
||||||
|
"encounter.ui.confirmEndCombat": "确定要结束这场战斗遭遇战吗?",
|
||||||
|
"encounter.ui.enemyDefaultEmoji": "👹",
|
||||||
|
"encounter.ui.yourActions": "你的行动",
|
||||||
|
"encounter.ui.attackType.aoe": "范围效果",
|
||||||
|
"encounter.ui.attackType.both": "单体或范围",
|
||||||
|
"encounter.ui.attackType.single": "单体目标",
|
||||||
|
"encounter.ui.targetingAllEnemies": " targeting all enemies!",
|
||||||
|
"encounter.ui.on": " on ",
|
||||||
|
"encounter.ui.youPrefix": "你: ",
|
||||||
|
"global.locked": "已锁定",
|
||||||
|
"global.unlocked": "已解锁",
|
||||||
|
"global.confirm": "确认",
|
||||||
|
"inventory.addItemPlaceholder": "输入物品名称...",
|
||||||
|
"inventory.stored.removeLocationConfirm": "删除\"{location}\"?这将删除该位置存储的所有物品。",
|
||||||
|
"userStats.clickToEdit": "点击编辑",
|
||||||
|
"quests.main.addQuestTitle": "添加主线任务",
|
||||||
|
"quests.optional.addQuestTitle": "添加可选任务"
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
{
|
{
|
||||||
"settings.language.label": "語言",
|
"settings.language.label": "語言",
|
||||||
"settings.language.option.en": "English",
|
"settings.language.option.en": "English",
|
||||||
|
"settings.language.option.zh-cn": "简体中文",
|
||||||
"settings.language.option.zh-tw": "繁體中文",
|
"settings.language.option.zh-tw": "繁體中文",
|
||||||
|
"settings.language.option.ru": "Русский",
|
||||||
|
"settings.language.option.fr": "Français",
|
||||||
"settings.extensionEnabled": "啟用 RPG Companion",
|
"settings.extensionEnabled": "啟用 RPG Companion",
|
||||||
"settings.note": "切換開關以啟用/停用 RPG Companion。其他設定可在面板內配置。",
|
"settings.note": "切換開關以啟用/停用 RPG Companion。其他設定可在面板內配置。",
|
||||||
"template.settingsTitle": "RPG Companion 設定",
|
"template.settingsTitle": "RPG Companion 設定",
|
||||||
@@ -29,11 +32,19 @@
|
|||||||
"template.settingsModal.display.showUserStats": "顯示 user 屬性",
|
"template.settingsModal.display.showUserStats": "顯示 user 屬性",
|
||||||
"template.settingsModal.display.showInfoBox": "顯示資訊框",
|
"template.settingsModal.display.showInfoBox": "顯示資訊框",
|
||||||
"template.settingsModal.display.showPresentCharacters": "顯示在場角色",
|
"template.settingsModal.display.showPresentCharacters": "顯示在場角色",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharacters": "顯示聊天下方的在場角色",
|
||||||
|
"template.settingsModal.display.showBelowChatPresentCharactersNote": "在聊天下方顯示精簡的在場角色面板。",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressions": "基於想法的表情",
|
||||||
|
"template.settingsModal.display.thoughtBasedExpressionsNote": "使用 SillyTavern Character Expressions 對聊天下方面板中每個在場角色的想法進行分類。Token 用量可能會依所選的分類 API 而增加。",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplay": "隱藏預設表情顯示",
|
||||||
|
"template.settingsModal.display.hideDefaultExpressionDisplayNote": "隱藏 SillyTavern 內建的角色表情顯示。",
|
||||||
"template.settingsModal.display.showInventory": "顯示物品欄",
|
"template.settingsModal.display.showInventory": "顯示物品欄",
|
||||||
"template.settingsModal.display.showLockIcons": "顯示鎖定/解鎖追蹤器",
|
"template.settingsModal.display.showLockIcons": "顯示鎖定/解鎖追蹤器",
|
||||||
"template.settingsModal.display.showLockIconsNote": "在追蹤器項目上顯示鎖定/解鎖圖示,以防止 AI 修改它們。",
|
"template.settingsModal.display.showLockIconsNote": "在追蹤器項目上顯示鎖定/解鎖圖示,以防止 AI 修改它們。",
|
||||||
"template.settingsModal.display.showThoughtsInChat": "在聊天中顯示想法",
|
"template.settingsModal.display.showThoughtsInChat": "在聊天中顯示想法",
|
||||||
"template.settingsModal.display.showThoughtsInChatNote": "將角色想法顯示為其訊息旁的泡泡",
|
"template.settingsModal.display.showThoughtsInChatNote": "將角色想法顯示為其訊息旁的泡泡",
|
||||||
|
"template.settingsModal.display.showInlineThoughts": "在訊息文字下方顯示想法",
|
||||||
|
"template.settingsModal.display.showInlineThoughtsNote": "在預設角落想法泡泡與顯示在訊息文字下方的想法卡片之間切換。",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubble": "始終顯示想法泡泡",
|
"template.settingsModal.display.alwaysShowThoughtBubble": "始終顯示想法泡泡",
|
||||||
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "自動展開想法泡泡",
|
"template.settingsModal.display.alwaysShowThoughtBubbleNote": "自動展開想法泡泡",
|
||||||
"template.settingsModal.display.enableAnimations": "啟用動畫",
|
"template.settingsModal.display.enableAnimations": "啟用動畫",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
* - Manual regeneration support
|
* - Manual regeneration support
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { generateRaw, characters, this_chid } from '../../../../../../../script.js';
|
import { characters, this_chid } from '../../../../../../../script.js';
|
||||||
|
import { safeGenerateRaw } from '../../utils/responseExtractor.js';
|
||||||
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
|
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
|
||||||
import { selected_group, getGroupMembers } from '../../../../../../group-chats.js';
|
import { selected_group, getGroupMembers } from '../../../../../../group-chats.js';
|
||||||
import { extensionSettings, sessionAvatarPrompts, setSessionAvatarPrompt } from '../../core/state.js';
|
import { extensionSettings, sessionAvatarPrompts, setSessionAvatarPrompt } from '../../core/state.js';
|
||||||
@@ -254,7 +255,7 @@ async function generateAvatarPrompt(characterName) {
|
|||||||
// console.log('[RPG Avatar] Using external API for avatar prompt generation');
|
// console.log('[RPG Avatar] Using external API for avatar prompt generation');
|
||||||
response = await generateWithExternalAPI(promptMessages);
|
response = await generateWithExternalAPI(promptMessages);
|
||||||
} else {
|
} else {
|
||||||
response = await generateRaw({
|
response = await safeGenerateRaw({
|
||||||
prompt: promptMessages,
|
prompt: promptMessages,
|
||||||
quietToLoud: false
|
quietToLoud: false
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
$userStatsContainer
|
$userStatsContainer
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveSettings, saveChatData } from '../../core/persistence.js';
|
import { saveSettings, saveChatData } from '../../core/persistence.js';
|
||||||
|
import { updateFabWidgets } from '../ui/mobile.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up event listeners for classic stat +/- buttons using delegation.
|
* Sets up event listeners for classic stat +/- buttons using delegation.
|
||||||
@@ -19,24 +20,34 @@ export function setupClassicStatsButtons() {
|
|||||||
// Delegated event listener for increase buttons
|
// Delegated event listener for increase buttons
|
||||||
$userStatsContainer.on('click', '.rpg-stat-increase', function() {
|
$userStatsContainer.on('click', '.rpg-stat-increase', function() {
|
||||||
const stat = $(this).data('stat');
|
const stat = $(this).data('stat');
|
||||||
|
// Initialize custom attributes if they don't exist
|
||||||
|
if (extensionSettings.classicStats[stat] === undefined) {
|
||||||
|
extensionSettings.classicStats[stat] = 10;
|
||||||
|
}
|
||||||
if (extensionSettings.classicStats[stat] < 999) {
|
if (extensionSettings.classicStats[stat] < 999) {
|
||||||
extensionSettings.classicStats[stat]++;
|
extensionSettings.classicStats[stat]++;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
saveChatData();
|
saveChatData();
|
||||||
// Update only the specific stat value, not the entire stats panel
|
// Update only the specific stat value, not the entire stats panel
|
||||||
$(this).closest('.rpg-classic-stat').find('.rpg-classic-stat-value').text(extensionSettings.classicStats[stat]);
|
$(this).closest('.rpg-classic-stat').find('.rpg-classic-stat-value').text(extensionSettings.classicStats[stat]);
|
||||||
|
updateFabWidgets();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delegated event listener for decrease buttons
|
// Delegated event listener for decrease buttons
|
||||||
$userStatsContainer.on('click', '.rpg-stat-decrease', function() {
|
$userStatsContainer.on('click', '.rpg-stat-decrease', function() {
|
||||||
const stat = $(this).data('stat');
|
const stat = $(this).data('stat');
|
||||||
|
// Initialize custom attributes if they don't exist
|
||||||
|
if (extensionSettings.classicStats[stat] === undefined) {
|
||||||
|
extensionSettings.classicStats[stat] = 10;
|
||||||
|
}
|
||||||
if (extensionSettings.classicStats[stat] > 1) {
|
if (extensionSettings.classicStats[stat] > 1) {
|
||||||
extensionSettings.classicStats[stat]--;
|
extensionSettings.classicStats[stat]--;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
saveChatData();
|
saveChatData();
|
||||||
// Update only the specific stat value, not the entire stats panel
|
// Update only the specific stat value, not the entire stats panel
|
||||||
$(this).closest('.rpg-classic-stat').find('.rpg-classic-stat-value').text(extensionSettings.classicStats[stat]);
|
$(this).closest('.rpg-classic-stat').find('.rpg-classic-stat-value').text(extensionSettings.classicStats[stat]);
|
||||||
|
updateFabWidgets();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export async function ensureJsonCleaningRegex(st_extension_settings, saveSetting
|
|||||||
}
|
}
|
||||||
// Small delay to ensure save completes
|
// Small delay to ensure save completes
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
console.log('[RPG Companion] ✅ Updated JSON cleaning regex to v3.2.6 settings.');
|
console.log('[RPG Companion] ✅ Updated JSON cleaning regex to v3.2.3 settings.');
|
||||||
} else {
|
} else {
|
||||||
console.log('[RPG Companion] JSON Cleaning Regex is up to date.');
|
console.log('[RPG Companion] JSON Cleaning Regex is up to date.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
|
|
||||||
import { togglePlotButtons } from '../ui/layout.js';
|
import { togglePlotButtons } from '../ui/layout.js';
|
||||||
import { extensionSettings, setIsPlotProgression } from '../../core/state.js';
|
import { extensionSettings, setIsPlotProgression } from '../../core/state.js';
|
||||||
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT } from '../generation/promptBuilder.js';
|
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_DECEPTION_PROMPT, DEFAULT_CYOA_PROMPT } from '../generation/promptBuilder.js';
|
||||||
import { Generate } from '../../../../../../../script.js';
|
import { Generate } from '../../../../../../../script.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the plot progression buttons inside the send form area.
|
* Sets up the plot progression buttons inside the send form area.
|
||||||
@@ -34,8 +35,8 @@ export function setupPlotButtons(handlePlotClick, handleEncounterClick) {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
" tabindex="0" role="button" title="Generate a random plot twist or event">
|
" tabindex="0" role="button" title="${i18n.getTranslation('plotProgression.tooltips.randomizedPlot') || 'Generate a random plot twist or event'}">
|
||||||
<i class="fa-solid fa-dice"></i> <span class="rpg-btn-text">Randomized Plot</span>
|
<i class="fa-solid fa-dice"></i> <span class="rpg-btn-text">${i18n.getTranslation('plotProgression.buttons.randomizedPlot') || 'Randomized Plot'}</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="rpg-plot-natural" class="menu_button interactable" style="
|
<button id="rpg-plot-natural" class="menu_button interactable" style="
|
||||||
background-color: #4a90e2;
|
background-color: #4a90e2;
|
||||||
@@ -46,8 +47,8 @@ export function setupPlotButtons(handlePlotClick, handleEncounterClick) {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
" tabindex="0" role="button" title="Continue the story naturally without twists">
|
" tabindex="0" role="button" title="${i18n.getTranslation('plotProgression.tooltips.naturalPlot') || 'Continue the story naturally without twists'}">
|
||||||
<i class="fa-solid fa-forward"></i> <span class="rpg-btn-text">Natural Plot</span>
|
<i class="fa-solid fa-forward"></i> <span class="rpg-btn-text">${i18n.getTranslation('plotProgression.buttons.naturalPlot') || 'Natural Plot'}</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="rpg-encounter-button" class="menu_button interactable" style="
|
<button id="rpg-encounter-button" class="menu_button interactable" style="
|
||||||
background-color: #cc3333;
|
background-color: #cc3333;
|
||||||
@@ -58,8 +59,8 @@ export function setupPlotButtons(handlePlotClick, handleEncounterClick) {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
" tabindex="0" role="button" title="Enter combat encounter">
|
" tabindex="0" role="button" title="${i18n.getTranslation('plotProgression.tooltips.enterEncounter') || 'Enter combat encounter'}">
|
||||||
<i class="fa-solid fa-fire"></i> <span class="rpg-btn-text">Enter Encounter</span>
|
<i class="fa-solid fa-fire"></i> <span class="rpg-btn-text">${i18n.getTranslation('plotProgression.buttons.enterEncounter') || 'Enter Encounter'}</span>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
@@ -121,6 +122,20 @@ export async function sendPlotProgression(type) {
|
|||||||
prompt += '\n\n' + dialogueColoringPromptText;
|
prompt += '\n\n' + dialogueColoringPromptText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Deception System prompt if enabled
|
||||||
|
if (extensionSettings.enableDeceptionSystem) {
|
||||||
|
// Use custom Deception prompt if set, otherwise use default
|
||||||
|
const deceptionPromptText = extensionSettings.customDeceptionPrompt || DEFAULT_DECEPTION_PROMPT;
|
||||||
|
prompt += '\n\n' + deceptionPromptText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add CYOA prompt if enabled
|
||||||
|
if (extensionSettings.enableCYOA) {
|
||||||
|
// Use custom CYOA prompt if set, otherwise use default
|
||||||
|
const cyoaPromptText = extensionSettings.customCYOAPrompt || DEFAULT_CYOA_PROMPT;
|
||||||
|
prompt += '\n\n' + cyoaPromptText;
|
||||||
|
}
|
||||||
|
|
||||||
// Set flag to indicate we're doing plot progression
|
// Set flag to indicate we're doing plot progression
|
||||||
// This will be used by onMessageReceived to clear the prompt after generation completes
|
// This will be used by onMessageReceived to clear the prompt after generation completes
|
||||||
setIsPlotProgression(true);
|
setIsPlotProgression(true);
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
* Handles API calls for RPG tracker generation
|
* Handles API calls for RPG tracker generation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { generateRaw, chat, eventSource } from '../../../../../../../script.js';
|
import { chat, eventSource } from '../../../../../../../script.js';
|
||||||
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
|
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
|
||||||
|
import { safeGenerateRaw, extractTextFromResponse } from '../../utils/responseExtractor.js';
|
||||||
|
|
||||||
// Custom event name for when RPG Companion finishes updating tracker data
|
// Custom event name for when RPG Companion finishes updating tracker data
|
||||||
// Other extensions can listen for this event to know when RPG Companion is done
|
// Other extensions can listen for this event to know when RPG Companion is done
|
||||||
@@ -17,9 +18,10 @@ import {
|
|||||||
lastActionWasSwipe,
|
lastActionWasSwipe,
|
||||||
setIsGenerating,
|
setIsGenerating,
|
||||||
setLastActionWasSwipe,
|
setLastActionWasSwipe,
|
||||||
$musicPlayerContainer
|
$musicPlayerContainer,
|
||||||
|
getSeparateGenerationId
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveChatData } from '../../core/persistence.js';
|
import { saveChatData, setMessageSwipeTrackerData } from '../../core/persistence.js';
|
||||||
import {
|
import {
|
||||||
generateSeparateUpdatePrompt
|
generateSeparateUpdatePrompt
|
||||||
} from './promptBuilder.js';
|
} from './promptBuilder.js';
|
||||||
@@ -30,10 +32,13 @@ 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';
|
||||||
import { generateAvatarsForCharacters } from '../features/avatarGenerator.js';
|
import { generateAvatarsForCharacters } from '../features/avatarGenerator.js';
|
||||||
|
import { setFabLoadingState, updateFabWidgets } from '../ui/mobile.js';
|
||||||
|
import { updateStripWidgets } from '../ui/desktop.js';
|
||||||
|
|
||||||
// Store the original preset name to restore after tracker generation
|
// Store the original preset name to restore after tracker generation
|
||||||
let originalPresetName = null;
|
let originalPresetName = null;
|
||||||
@@ -105,11 +110,10 @@ export async function generateWithExternalAPI(messages) {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!data.choices || !data.choices[0] || !data.choices[0].message) {
|
const content = extractTextFromResponse(data);
|
||||||
throw new Error('Invalid response format from external API');
|
if (!content || !content.trim()) {
|
||||||
|
throw new Error('Invalid response format from external API — no text content found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = data.choices[0].message.content;
|
|
||||||
// console.log('[RPG Companion] External API response received successfully');
|
// console.log('[RPG Companion] External API response received successfully');
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
@@ -216,7 +220,7 @@ export async function switchToPreset(presetName) {
|
|||||||
* @param {Function} renderThoughts - UI function to render character thoughts
|
* @param {Function} renderThoughts - UI function to render character thoughts
|
||||||
* @param {Function} renderInventory - UI function to render inventory
|
* @param {Function} renderInventory - UI function to render inventory
|
||||||
*/
|
*/
|
||||||
export async function updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory) {
|
export async function updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, generationId = null) {
|
||||||
if (isGenerating) {
|
if (isGenerating) {
|
||||||
// console.log('[RPG Companion] Already generating, skipping...');
|
// console.log('[RPG Companion] Already generating, skipping...');
|
||||||
return;
|
return;
|
||||||
@@ -235,11 +239,14 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
|
setFabLoadingState(true); // Show spinning FAB on mobile
|
||||||
|
|
||||||
// Update button to show "Updating..." state
|
// Update button to show "Updating..." state
|
||||||
const $updateBtn = $('#rpg-manual-update');
|
const $updateBtn = $('#rpg-manual-update');
|
||||||
|
const $stripRefreshBtn = $('#rpg-strip-refresh');
|
||||||
const updatingText = i18n.getTranslation('template.mainPanel.updating') || 'Updating...';
|
const updatingText = i18n.getTranslation('template.mainPanel.updating') || 'Updating...';
|
||||||
$updateBtn.html(`<i class="fa-solid fa-spinner fa-spin"></i> ${updatingText}`).prop('disabled', true);
|
$updateBtn.html(`<i class="fa-solid fa-spinner fa-spin"></i> ${updatingText}`).prop('disabled', true);
|
||||||
|
$stripRefreshBtn.html('<i class="fa-solid fa-spinner fa-spin"></i>').prop('disabled', true);
|
||||||
|
|
||||||
const prompt = await generateSeparateUpdatePrompt();
|
const prompt = await generateSeparateUpdatePrompt();
|
||||||
|
|
||||||
@@ -250,20 +257,28 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
// console.log('[RPG Companion] Using external API for tracker generation');
|
// console.log('[RPG Companion] Using external API for tracker generation');
|
||||||
response = await generateWithExternalAPI(prompt);
|
response = await generateWithExternalAPI(prompt);
|
||||||
} else {
|
} else {
|
||||||
// Separate mode: Use SillyTavern's generateRaw
|
// Separate mode: Use SillyTavern's generateRaw (with extended thinking fallback)
|
||||||
response = await generateRaw({
|
response = await safeGenerateRaw({
|
||||||
prompt: prompt,
|
prompt: prompt,
|
||||||
quietToLoud: false
|
quietToLoud: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a generationId was provided and the counter has since been incremented
|
||||||
|
// (by a deletion or a newer generation), discard this result entirely.
|
||||||
|
// The finally block still runs to restore button state.
|
||||||
|
if (generationId !== null && getSeparateGenerationId() !== generationId) {
|
||||||
|
// console.log('[RPG Companion] ⚠️ Separate generation result discarded — superseded (genId', generationId, '!= current', getSeparateGenerationId(), ')');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
// console.log('[RPG Companion] Raw AI response:', response);
|
// console.log('[RPG Companion] Raw AI response:', response);
|
||||||
const parsedData = parseResponse(response);
|
const parsedData = parseResponse(response);
|
||||||
|
|
||||||
// Check if parsing completely failed (no tracker data found)
|
// Check if parsing completely failed (no tracker data found)
|
||||||
if (parsedData.parsingFailed) {
|
if (parsedData.parsingFailed) {
|
||||||
toastr.error(i18n.getTranslation('errors.parsingError'), '', { timeOut: 5000 });
|
toastr.error(i18n.getTranslation('errors.parsingError') || 'RPG Companion Trackers parsing error! The model returned incorrect format. Consider switching generation model if this persists.', '', { timeOut: 5000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove locks from parsed data (JSON format only, text format is unaffected)
|
// Remove locks from parsed data (JSON format only, text format is unaffected)
|
||||||
@@ -302,21 +317,6 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
lastGeneratedData.characterThoughts = parsedData.characterThoughts;
|
lastGeneratedData.characterThoughts = parsedData.characterThoughts;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When saveTrackerHistory is enabled, store tracker data on the user's message too
|
|
||||||
// This allows scrolling through history and seeing trackers at each point
|
|
||||||
if (extensionSettings.saveTrackerHistory && lastMessage && lastMessage.is_user) {
|
|
||||||
if (!lastMessage.extra) {
|
|
||||||
lastMessage.extra = {};
|
|
||||||
}
|
|
||||||
lastMessage.extra.rpg_companion_data = {
|
|
||||||
userStats: parsedData.userStats,
|
|
||||||
infoBox: parsedData.infoBox,
|
|
||||||
characterThoughts: parsedData.characterThoughts,
|
|
||||||
timestamp: Date.now()
|
|
||||||
};
|
|
||||||
// console.log('[RPG Companion] 💾 Stored tracker data on user message for history');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also store on assistant message if present (existing behavior)
|
// Also store on assistant message if present (existing behavior)
|
||||||
if (lastMessage && !lastMessage.is_user) {
|
if (lastMessage && !lastMessage.is_user) {
|
||||||
if (!lastMessage.extra) {
|
if (!lastMessage.extra) {
|
||||||
@@ -327,11 +327,11 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentSwipeId = lastMessage.swipe_id || 0;
|
const currentSwipeId = lastMessage.swipe_id || 0;
|
||||||
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = {
|
setMessageSwipeTrackerData(lastMessage, currentSwipeId, {
|
||||||
userStats: parsedData.userStats,
|
userStats: parsedData.userStats,
|
||||||
infoBox: parsedData.infoBox,
|
infoBox: parsedData.infoBox,
|
||||||
characterThoughts: parsedData.characterThoughts
|
characterThoughts: parsedData.characterThoughts
|
||||||
};
|
});
|
||||||
|
|
||||||
// console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId);
|
// console.log('[RPG Companion] Stored separate mode RPG data for message swipe', currentSwipeId);
|
||||||
}
|
}
|
||||||
@@ -357,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]);
|
||||||
|
|
||||||
@@ -391,11 +392,16 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
|
setFabLoadingState(false); // Stop spinning FAB on mobile
|
||||||
|
updateFabWidgets(); // Update FAB widgets with new data
|
||||||
|
updateStripWidgets(); // Update strip widgets with new data
|
||||||
|
|
||||||
// Restore button to original state
|
// Restore button to original state
|
||||||
const $updateBtn = $('#rpg-manual-update');
|
const $updateBtn = $('#rpg-manual-update');
|
||||||
|
const $stripRefreshBtn = $('#rpg-strip-refresh');
|
||||||
const refreshText = i18n.getTranslation('template.mainPanel.refreshRpgInfo') || 'Refresh RPG Info';
|
const refreshText = i18n.getTranslation('template.mainPanel.refreshRpgInfo') || 'Refresh RPG Info';
|
||||||
$updateBtn.html(`<i class="fa-solid fa-sync"></i> ${refreshText}`).prop('disabled', false);
|
$updateBtn.html(`<i class="fa-solid fa-sync"></i> ${refreshText}`).prop('disabled', false);
|
||||||
|
$stripRefreshBtn.html('<i class="fa-solid fa-sync"></i>').prop('disabled', false);
|
||||||
|
|
||||||
// Reset the flag after tracker generation completes
|
// Reset the flag after tracker generation completes
|
||||||
// This ensures the flag persists through both main generation AND tracker generation
|
// This ensures the flag persists through both main generation AND tracker generation
|
||||||
@@ -416,6 +422,26 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
function parseCharactersFromThoughts(characterThoughtsData) {
|
function parseCharactersFromThoughts(characterThoughtsData) {
|
||||||
if (!characterThoughtsData) return [];
|
if (!characterThoughtsData) return [];
|
||||||
|
|
||||||
|
// Try parsing as JSON first (current format)
|
||||||
|
try {
|
||||||
|
const parsed = typeof characterThoughtsData === 'string'
|
||||||
|
? JSON.parse(characterThoughtsData)
|
||||||
|
: characterThoughtsData;
|
||||||
|
|
||||||
|
// Handle both {characters: [...]} and direct array formats
|
||||||
|
const charactersArray = Array.isArray(parsed) ? parsed : (parsed.characters || []);
|
||||||
|
|
||||||
|
if (charactersArray.length > 0) {
|
||||||
|
// Extract names from JSON character objects
|
||||||
|
return charactersArray
|
||||||
|
.map(char => char.name)
|
||||||
|
.filter(name => name && name.toLowerCase() !== 'unavailable');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not JSON, fall back to text parsing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Parse text format (legacy)
|
||||||
const lines = characterThoughtsData.split('\n');
|
const lines = characterThoughtsData.split('\n');
|
||||||
const characters = [];
|
const characters = [];
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { selected_group, getGroupMembers, groups } from '../../../../../../group
|
|||||||
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
||||||
import { currentEncounter } from '../features/encounterState.js';
|
import { currentEncounter } from '../features/encounterState.js';
|
||||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||||
|
import { isPresentCharactersEnabled } from '../../utils/presentCharacters.js';
|
||||||
import { buildInventorySummary, generateTrackerInstructions, generateTrackerExample } from './promptBuilder.js';
|
import { buildInventorySummary, generateTrackerInstructions, generateTrackerExample } from './promptBuilder.js';
|
||||||
import { applyLocks } from './lockManager.js';
|
import { applyLocks } from './lockManager.js';
|
||||||
|
|
||||||
@@ -121,7 +122,7 @@ export async function buildEncounterInitPrompt() {
|
|||||||
|
|
||||||
// console.log('[RPG Companion] World info result:', { worldInfoString, length: worldInfoString?.length });
|
// console.log('[RPG Companion] World info result:', { worldInfoString, length: worldInfoString?.length });
|
||||||
|
|
||||||
if (worldInfoString && worldInfoString.trim()) {
|
if (worldInfoString && typeof worldInfoString === 'string' && worldInfoString.trim()) {
|
||||||
systemMessage += worldInfoString.trim();
|
systemMessage += worldInfoString.trim();
|
||||||
worldInfoAdded = true;
|
worldInfoAdded = true;
|
||||||
// console.log('[RPG Companion] ✅ Added world info from getWorldInfoPrompt');
|
// console.log('[RPG Companion] ✅ Added world info from getWorldInfoPrompt');
|
||||||
@@ -258,6 +259,7 @@ export async function buildEncounterInitPrompt() {
|
|||||||
|
|
||||||
initInstruction += `The combat starts now.\n\n`;
|
initInstruction += `The combat starts now.\n\n`;
|
||||||
initInstruction += `Based on everything above, generate the initial combat state. Analyze who is in the party fighting alongside ${userName} (if anyone), and who the enemies are. Replace placeholders in [brackets] and X with actual values. Return ONLY a JSON object with the following structure:\n\n`;
|
initInstruction += `Based on everything above, generate the initial combat state. Analyze who is in the party fighting alongside ${userName} (if anyone), and who the enemies are. Replace placeholders in [brackets] and X with actual values. Return ONLY a JSON object with the following structure:\n\n`;
|
||||||
|
initInstruction += `FORMAT:\n`;
|
||||||
initInstruction += `{\n`;
|
initInstruction += `{\n`;
|
||||||
initInstruction += ` "party": [\n`;
|
initInstruction += ` "party": [\n`;
|
||||||
initInstruction += ` {\n`;
|
initInstruction += ` {\n`;
|
||||||
@@ -268,7 +270,7 @@ export async function buildEncounterInitPrompt() {
|
|||||||
initInstruction += ` {"name": "Attack", "type": "single-target|AoE|both"},\n`;
|
initInstruction += ` {"name": "Attack", "type": "single-target|AoE|both"},\n`;
|
||||||
initInstruction += ` {"name": "Skill1", "type": "single-target|AoE|both"}\n`;
|
initInstruction += ` {"name": "Skill1", "type": "single-target|AoE|both"}\n`;
|
||||||
initInstruction += ` ],\n`;
|
initInstruction += ` ],\n`;
|
||||||
initInstruction += ` "items": ["Item1", "Item2"],\n`;
|
initInstruction += ` "items": ["Item Name x3", "Another Item x1"],\n`;
|
||||||
initInstruction += ` "statuses": [],\n`;
|
initInstruction += ` "statuses": [],\n`;
|
||||||
initInstruction += ` "isPlayer": true\n`;
|
initInstruction += ` "isPlayer": true\n`;
|
||||||
initInstruction += ` }\n`;
|
initInstruction += ` }\n`;
|
||||||
@@ -302,11 +304,14 @@ export async function buildEncounterInitPrompt() {
|
|||||||
initInstruction += ` - "single-target": Can only target one character (enemy or ally)\n`;
|
initInstruction += ` - "single-target": Can only target one character (enemy or ally)\n`;
|
||||||
initInstruction += ` - "AoE": Area of Effect - targets all enemies, but some AoE attacks (like storms, explosions) can also harm allies if the attack is indiscriminate\n`;
|
initInstruction += ` - "AoE": Area of Effect - targets all enemies, but some AoE attacks (like storms, explosions) can also harm allies if the attack is indiscriminate\n`;
|
||||||
initInstruction += ` - "both": Player can choose to target a single enemy OR use as AoE\n`;
|
initInstruction += ` - "both": Player can choose to target a single enemy OR use as AoE\n`;
|
||||||
|
initInstruction += `- For items array: Include quantities using format "Item Name xN" (e.g., "Health Potion x3", "Bomb x1")\n`;
|
||||||
|
initInstruction += ` - If only one item exists, you can use "Item Name x1" or just "Item Name"\n`;
|
||||||
|
initInstruction += ` - Items will be consumed when used - the quantity will decrease in future turns\n`;
|
||||||
initInstruction += `- Statuses array: May start empty, but don't have to if characters applied them before the combat\n`;
|
initInstruction += `- Statuses array: May start empty, but don't have to if characters applied them before the combat\n`;
|
||||||
initInstruction += ` - Each status has a format: {"name": "Status Name", "emoji": "💀", "duration": X}\n`;
|
initInstruction += ` - Each status has a format: {"name": "Status Name", "emoji": "💀", "duration": X}\n`;
|
||||||
initInstruction += ` - Examples: Poisoned (🧪), Burning (🔥), Blessed (✨), Stunned (💫), Weakened (⬇️), Strengthened (⬆️)\n\n`;
|
initInstruction += ` - Examples: Poisoned (🧪), Burning (🔥), Blessed (✨), Stunned (💫), Weakened (⬇️), Strengthened (⬆️)\n\n`;
|
||||||
initInstruction += `The styleNotes object will be used to visually style the combat window - choose ONE value from each category that best fits the environment described in the chat history.\n\n`;
|
initInstruction += `The styleNotes object will be used to visually style the combat window - choose ONE value from each category that best fits the environment described in the chat history.\n\n`;
|
||||||
initInstruction += `Use the user's current stats, inventory, and skills to populate the party data. For ${userName}'s attacks array, include their available skills. For items, include usable items from their inventory. Set HP based on their current Health stat if available.\n\n`;
|
initInstruction += `Use the user's current stats, inventory, and skills to populate the party data. For ${userName}'s attacks array, include their available skills. For items, include usable items from their inventory WITH QUANTITIES (e.g., "Health Potion x3"). Set HP based on their current Health stat if available.\n\n`;
|
||||||
initInstruction += `Ensure all party members and enemies have realistic HP values based on the setting and their descriptions. Return ONLY the JSON object, no other text.`;
|
initInstruction += `Ensure all party members and enemies have realistic HP values based on the setting and their descriptions. Return ONLY the JSON object, no other text.`;
|
||||||
|
|
||||||
// Only add the instruction if it has meaningful content
|
// Only add the instruction if it has meaningful content
|
||||||
@@ -364,7 +369,7 @@ export async function buildCombatActionPrompt(action, combatStats) {
|
|||||||
const result = await getWorldInfoFn(chatForWI, 8000, false);
|
const result = await getWorldInfoFn(chatForWI, 8000, false);
|
||||||
const worldInfoString = result?.worldInfoString || result;
|
const worldInfoString = result?.worldInfoString || result;
|
||||||
|
|
||||||
if (worldInfoString && worldInfoString.trim()) {
|
if (worldInfoString && typeof worldInfoString === 'string' && worldInfoString.trim()) {
|
||||||
systemMessage += worldInfoString.trim();
|
systemMessage += worldInfoString.trim();
|
||||||
worldInfoAdded = true;
|
worldInfoAdded = true;
|
||||||
}
|
}
|
||||||
@@ -483,12 +488,25 @@ export async function buildCombatActionPrompt(action, combatStats) {
|
|||||||
stateMessage += `Party Members:\n`;
|
stateMessage += `Party Members:\n`;
|
||||||
combatStats.party.forEach(member => {
|
combatStats.party.forEach(member => {
|
||||||
stateMessage += `- ${member.name}${member.isPlayer ? ' (Player)' : ''}: ${member.hp}/${member.maxHp} HP\n`;
|
stateMessage += `- ${member.name}${member.isPlayer ? ' (Player)' : ''}: ${member.hp}/${member.maxHp} HP\n`;
|
||||||
if (member.attacks && member.attacks.length > 0) {
|
|
||||||
stateMessage += ` Attacks: ${member.attacks.map(a => typeof a === 'string' ? a : a.name).join(', ')}\n`;
|
// For the player, use playerActions if available, otherwise fall back to member data
|
||||||
}
|
if (member.isPlayer && currentEncounter.playerActions) {
|
||||||
if (member.items && member.items.length > 0) {
|
if (currentEncounter.playerActions.attacks && currentEncounter.playerActions.attacks.length > 0) {
|
||||||
stateMessage += ` Items: ${member.items.join(', ')}\n`;
|
stateMessage += ` Attacks: ${currentEncounter.playerActions.attacks.map(a => typeof a === 'string' ? a : a.name).join(', ')}\n`;
|
||||||
|
}
|
||||||
|
if (currentEncounter.playerActions.items && currentEncounter.playerActions.items.length > 0) {
|
||||||
|
stateMessage += ` Items: ${currentEncounter.playerActions.items.join(', ')}\n`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For non-player party members, use their own data
|
||||||
|
if (member.attacks && member.attacks.length > 0) {
|
||||||
|
stateMessage += ` Attacks: ${member.attacks.map(a => typeof a === 'string' ? a : a.name).join(', ')}\n`;
|
||||||
|
}
|
||||||
|
if (member.items && member.items.length > 0) {
|
||||||
|
stateMessage += ` Items: ${member.items.join(', ')}\n`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (member.statuses && member.statuses.length > 0) {
|
if (member.statuses && member.statuses.length > 0) {
|
||||||
const validStatuses = member.statuses.filter(s => s && (s.emoji || s.name));
|
const validStatuses = member.statuses.filter(s => s && (s.emoji || s.name));
|
||||||
if (validStatuses.length > 0) {
|
if (validStatuses.length > 0) {
|
||||||
@@ -515,11 +533,39 @@ export async function buildCombatActionPrompt(action, combatStats) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
stateMessage += `\n${userName}'s Action: ${action}\n\n`;
|
stateMessage += `\n${userName}'s Action: ${action}\n\n`;
|
||||||
stateMessage += `Respond with the exact JSON object as below, containing ONLY these specified values. Remember to consider the user's party and their moves. DO NOT regenerate character descriptions, sprites, or environment:\n`;
|
stateMessage += `Respond with the exact JSON object as below, containing ONLY these specified values. Remember to consider the user's party and their moves. DO NOT regenerate character descriptions, sprites, or environment.\n\n`;
|
||||||
|
stateMessage += `IMPORTANT - Update ${userName}'s attacks and items arrays based on what happens in combat:\n`;
|
||||||
|
stateMessage += `- ${userName}'s action is already specified above - do NOT regenerate it. Only update ${userName}'s attacks/items arrays if their action consumed resources (used item, lost ability, etc.).\n`;
|
||||||
|
stateMessage += `- If they use an item, decrement its quantity ("Health Potion x3" becomes "Health Potion x2"). If quantity reaches 0, remove the item entirely.\n`;
|
||||||
|
stateMessage += `- If they gain or lose an ability due to status effects, add or remove it from their attacks array.\n`;
|
||||||
|
stateMessage += ` Examples: Disarmed → remove weapon attacks. Bound → remove all attacks or set to []. Freed → restore attacks.\n`;
|
||||||
|
stateMessage += `- If they pick up a weapon/item during combat, add it to their items or attacks array.\n`;
|
||||||
|
stateMessage += `- If environmental changes enable new actions (near water → "Splash Attack"), add them. If they disable actions (fire goes out → remove "Ignite"), remove them.\n`;
|
||||||
|
stateMessage += `- Status effects should persist and decrease duration each turn. Remove statuses when duration reaches 0.\n\n`;
|
||||||
|
stateMessage += `FORMAT:\n`;
|
||||||
stateMessage += `{\n`;
|
stateMessage += `{\n`;
|
||||||
stateMessage += ` "combatStats": {\n`;
|
stateMessage += ` "combatStats": {\n`;
|
||||||
stateMessage += ` "party": [{ "name": "Name", "hp": X, "maxHp": X, "statuses": [...] }],\n`;
|
stateMessage += ` "party": [\n`;
|
||||||
stateMessage += ` "enemies": [{ "name": "Name", "hp": X, "maxHp": X, "statuses": [...] }]\n`;
|
stateMessage += ` {\n`;
|
||||||
|
stateMessage += ` "name": "Name",\n`;
|
||||||
|
stateMessage += ` "hp": X,\n`;
|
||||||
|
stateMessage += ` "maxHp": X,\n`;
|
||||||
|
stateMessage += ` "statuses": [{"name": "Status", "emoji": "💀", "duration": X}],\n`;
|
||||||
|
stateMessage += ` "isPlayer": true|false\n`;
|
||||||
|
stateMessage += ` }\n`;
|
||||||
|
stateMessage += ` ],\n`;
|
||||||
|
stateMessage += ` "enemies": [\n`;
|
||||||
|
stateMessage += ` {\n`;
|
||||||
|
stateMessage += ` "name": "Name",\n`;
|
||||||
|
stateMessage += ` "hp": X,\n`;
|
||||||
|
stateMessage += ` "maxHp": X,\n`;
|
||||||
|
stateMessage += ` "statuses": [{"name": "Status", "emoji": "💀", "duration": X}]\n`;
|
||||||
|
stateMessage += ` }\n`;
|
||||||
|
stateMessage += ` ]\n`;
|
||||||
|
stateMessage += ` },\n`;
|
||||||
|
stateMessage += ` "playerActions": {\n`;
|
||||||
|
stateMessage += ` "attacks": [{"name": "Attack", "type": "single-target|AoE|both"}],\n`;
|
||||||
|
stateMessage += ` "items": ["Item Name x3", "Another Item x1"]\n`;
|
||||||
stateMessage += ` },\n`;
|
stateMessage += ` },\n`;
|
||||||
stateMessage += ` "enemyActions": [{ "enemyName": "Name", "action": "what they do", "target": "target" }],\n`;
|
stateMessage += ` "enemyActions": [{ "enemyName": "Name", "action": "what they do", "target": "target" }],\n`;
|
||||||
stateMessage += ` "partyActions": [{ "memberName": "Name", "action": "what they do", "target": "target" }],\n`;
|
stateMessage += ` "partyActions": [{ "memberName": "Name", "action": "what they do", "target": "target" }],\n`;
|
||||||
@@ -587,7 +633,7 @@ export async function buildCombatSummaryPrompt(combatLog, result) {
|
|||||||
const result = await getWorldInfoFn(chatForWI, 8000, false);
|
const result = await getWorldInfoFn(chatForWI, 8000, false);
|
||||||
const worldInfoString = result?.worldInfoString || result;
|
const worldInfoString = result?.worldInfoString || result;
|
||||||
|
|
||||||
if (worldInfoString && worldInfoString.trim()) {
|
if (worldInfoString && typeof worldInfoString === 'string' && worldInfoString.trim()) {
|
||||||
systemMessage += worldInfoString.trim();
|
systemMessage += worldInfoString.trim();
|
||||||
worldInfoAdded = true;
|
worldInfoAdded = true;
|
||||||
}
|
}
|
||||||
@@ -659,10 +705,12 @@ export async function buildCombatSummaryPrompt(combatLog, result) {
|
|||||||
summaryMessage += `\n\nProvide a narrative summary of the entire fight in a way that fits the style from the chat history. Start with [FIGHT CONCLUDED] on the first line, then provide the description.\n\n`;
|
summaryMessage += `\n\nProvide a narrative summary of the entire fight in a way that fits the style from the chat history. Start with [FIGHT CONCLUDED] on the first line, then provide the description.\n\n`;
|
||||||
summaryMessage += `Write with intent in ${tense} tense ${person}-person ${narration} from ${pov}'s point of view.\n`;
|
summaryMessage += `Write with intent in ${tense} tense ${person}-person ${narration} from ${pov}'s point of view.\n`;
|
||||||
summaryMessage += `Build novel prose. Break patterns from your previous responses by varying sentence structures, rhythms, formats, and openings. If you last started with a narration, begin this one with dialogue; if with an action, switch to an internal thought. Track whether a sentence shape, cadence, sensory detail, or a descriptor appeared recently, and replace it with a different one or skip it entirely. If you mentioned a physical feature, mannerism, or expression once, move on. Don't fixate on the same traits every turn. Well-placed humor, unconventional similes, and subverted turns of phrase are encouraged. Avoid GPTisms, purple prose, and anaphoras, especially the negatives with "no/not/wasn't/didn't." Refrain from describing actions or emotions via negation; focus on what does happen, not what doesn't. Minimize asterisks, ellipses, and em-dashes. Explicit content is allowed. Never end on handover cues; finish naturally.\n\n`;
|
summaryMessage += `Build novel prose. Break patterns from your previous responses by varying sentence structures, rhythms, formats, and openings. If you last started with a narration, begin this one with dialogue; if with an action, switch to an internal thought. Track whether a sentence shape, cadence, sensory detail, or a descriptor appeared recently, and replace it with a different one or skip it entirely. If you mentioned a physical feature, mannerism, or expression once, move on. Don't fixate on the same traits every turn. Well-placed humor, unconventional similes, and subverted turns of phrase are encouraged. Avoid GPTisms, purple prose, and anaphoras, especially the negatives with "no/not/wasn't/didn't." Refrain from describing actions or emotions via negation; focus on what does happen, not what doesn't. Minimize asterisks, ellipses, and em-dashes. Explicit content is allowed. Never end on handover cues; finish naturally.\n\n`;
|
||||||
summaryMessage += `Express ${userName}'s actions and dialogue using indirect speech (e.g., "${userName} swung their sword" or "${userName} asked for help"). The summary should be 2-4 paragraphs and capture the essence of the battle.\n\n`;
|
summaryMessage += `Dialogue Guidelines:\n`;
|
||||||
|
summaryMessage += `- Include ALL dialogue lines spoken by enemies and NPC party members during the encounter in direct quotes.\n`;
|
||||||
|
summaryMessage += `- Never quote ${userName} directly. Express their actions and dialogue using ONLY indirect speech (e.g., "${userName} swung their sword" or "${userName} asked for help").\n\n`;
|
||||||
|
|
||||||
// If in Together mode and trackers are enabled, add tracker update instructions
|
// If in Together mode and trackers are enabled, add tracker update instructions
|
||||||
if (extensionSettings.generationMode === 'together' && (extensionSettings.showUserStats || extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts)) {
|
if (extensionSettings.generationMode === 'together' && (extensionSettings.showUserStats || extensionSettings.showInfoBox || isPresentCharactersEnabled())) {
|
||||||
summaryMessage += `\n--- TRACKER UPDATE ---\n\n`;
|
summaryMessage += `\n--- TRACKER UPDATE ---\n\n`;
|
||||||
summaryMessage += `After the [FIGHT CONCLUDED] summary, update the RPG trackers to reflect ${userName}'s state AFTER the combat encounter. `;
|
summaryMessage += `After the [FIGHT CONCLUDED] summary, update the RPG trackers to reflect ${userName}'s state AFTER the combat encounter. `;
|
||||||
summaryMessage += `Account for any injuries sustained, resources used, emotional state changes, or other consequences of the battle.\n\n`;
|
summaryMessage += `Account for any injuries sustained, resources used, emotional state changes, or other consequences of the battle.\n\n`;
|
||||||
@@ -721,6 +769,12 @@ export async function buildCombatSummaryPrompt(combatLog, result) {
|
|||||||
*/
|
*/
|
||||||
export function parseEncounterJSON(response) {
|
export function parseEncounterJSON(response) {
|
||||||
try {
|
try {
|
||||||
|
// Ensure response is a string
|
||||||
|
if (!response || typeof response !== 'string') {
|
||||||
|
console.error('[RPG Companion] parseEncounterJSON received non-string input:', typeof response);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove code blocks if present
|
// Remove code blocks if present
|
||||||
let cleaned = response.trim();
|
let cleaned = response.trim();
|
||||||
|
|
||||||
@@ -736,6 +790,9 @@ export function parseEncounterJSON(response) {
|
|||||||
|
|
||||||
if (firstBrace !== -1 && lastBrace !== -1) {
|
if (firstBrace !== -1 && lastBrace !== -1) {
|
||||||
cleaned = cleaned.substring(firstBrace, lastBrace + 1);
|
cleaned = cleaned.substring(firstBrace, lastBrace + 1);
|
||||||
|
} else {
|
||||||
|
console.error('[RPG Companion] No JSON object found in response');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to parse directly first
|
// Try to parse directly first
|
||||||
|
|||||||
+661
-112
@@ -4,15 +4,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { setExtensionPrompt, extension_prompt_types, extension_prompt_roles } from '../../../../../../../script.js';
|
import { extension_prompt_types, extension_prompt_roles, setExtensionPrompt, eventSource, event_types } from '../../../../../../../script.js';
|
||||||
import {
|
import {
|
||||||
extensionSettings,
|
extensionSettings,
|
||||||
committedTrackerData,
|
committedTrackerData,
|
||||||
lastGeneratedData,
|
lastGeneratedData,
|
||||||
isGenerating,
|
isGenerating,
|
||||||
lastActionWasSwipe,
|
lastActionWasSwipe
|
||||||
setLastActionWasSwipe,
|
|
||||||
setIsGenerating
|
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { evaluateSuppression } from './suppression.js';
|
import { evaluateSuppression } from './suppression.js';
|
||||||
import { parseUserStats } from './parser.js';
|
import { parseUserStats } from './parser.js';
|
||||||
@@ -20,18 +18,530 @@ import {
|
|||||||
generateTrackerExample,
|
generateTrackerExample,
|
||||||
generateTrackerInstructions,
|
generateTrackerInstructions,
|
||||||
generateContextualSummary,
|
generateContextualSummary,
|
||||||
|
formatHistoricalTrackerData,
|
||||||
DEFAULT_HTML_PROMPT,
|
DEFAULT_HTML_PROMPT,
|
||||||
DEFAULT_DIALOGUE_COLORING_PROMPT,
|
DEFAULT_DIALOGUE_COLORING_PROMPT,
|
||||||
|
DEFAULT_DECEPTION_PROMPT,
|
||||||
|
DEFAULT_OMNISCIENCE_FILTER_PROMPT,
|
||||||
|
DEFAULT_CYOA_PROMPT,
|
||||||
DEFAULT_SPOTIFY_PROMPT,
|
DEFAULT_SPOTIFY_PROMPT,
|
||||||
|
DEFAULT_NARRATOR_PROMPT,
|
||||||
|
DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT,
|
||||||
SPOTIFY_FORMAT_INSTRUCTION
|
SPOTIFY_FORMAT_INSTRUCTION
|
||||||
} from './promptBuilder.js';
|
} from './promptBuilder.js';
|
||||||
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||||
|
import { commitTrackerDataFromPriorMessage } from '../../core/persistence.js';
|
||||||
|
|
||||||
|
// Track suppression state for event handler
|
||||||
|
let currentSuppressionState = false;
|
||||||
|
|
||||||
// Type imports
|
// Type imports
|
||||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||||
|
|
||||||
// Track last chat length we committed at to prevent duplicate commits from streaming
|
// Track the latest user message we committed for to prevent duplicate commits
|
||||||
let lastCommittedChatLength = -1;
|
// when GENERATION_STARTED can fire multiple times for the same turn.
|
||||||
|
let lastCommittedUserMessageSignature = null;
|
||||||
|
|
||||||
|
// Store context map for prompt injection (used by event handlers)
|
||||||
|
let pendingContextMap = new Map();
|
||||||
|
|
||||||
|
// Flag to track if injection already happened in BEFORE_COMBINE
|
||||||
|
let historyInjectionDone = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a map of historical context data from ST chat messages with rpg_companion_swipes data.
|
||||||
|
* Returns a map keyed by message index with formatted context strings.
|
||||||
|
* The index stored depends on the injection position setting.
|
||||||
|
*
|
||||||
|
* @returns {Map<number, string>} Map of target message index to formatted context string
|
||||||
|
*/
|
||||||
|
function buildHistoricalContextMap() {
|
||||||
|
const historyPersistence = extensionSettings.historyPersistence;
|
||||||
|
if (!historyPersistence || !historyPersistence.enabled) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context.chat;
|
||||||
|
if (!chat || chat.length < 2) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackerConfig = extensionSettings.trackerConfig;
|
||||||
|
const userName = context.name1;
|
||||||
|
const position = historyPersistence.injectionPosition || 'assistant_message_end';
|
||||||
|
const contextMap = new Map();
|
||||||
|
|
||||||
|
// Determine how many messages to include (0 = all available)
|
||||||
|
const messageCount = historyPersistence.messageCount || 0;
|
||||||
|
const maxMessages = messageCount === 0 ? chat.length : Math.min(messageCount, chat.length);
|
||||||
|
|
||||||
|
// Find the last assistant message - this is the one that gets current context via setExtensionPrompt
|
||||||
|
// We should NOT add historical context to it when injecting into assistant messages
|
||||||
|
// But when injecting into user messages, we DO need to process it to get context for the preceding user message
|
||||||
|
let lastAssistantIndex = -1;
|
||||||
|
for (let i = chat.length - 1; i >= 0; i--) {
|
||||||
|
if (!chat[i].is_user && !chat[i].is_system) {
|
||||||
|
lastAssistantIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through messages to find those with tracker data
|
||||||
|
// For user_message_end: start from the last assistant message (we need its context for the preceding user message)
|
||||||
|
// For assistant_message_end: start from before the last assistant message (it gets current context via setExtensionPrompt)
|
||||||
|
let processedCount = 0;
|
||||||
|
const startIndex = position === 'user_message_end'
|
||||||
|
? lastAssistantIndex
|
||||||
|
: (lastAssistantIndex > 0 ? lastAssistantIndex - 1 : chat.length - 2);
|
||||||
|
|
||||||
|
for (let i = startIndex; i >= 0 && (messageCount === 0 || processedCount < maxMessages); i--) {
|
||||||
|
const message = chat[i];
|
||||||
|
|
||||||
|
// Skip system messages
|
||||||
|
if (message.is_system) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only assistant messages have rpg_companion_swipes data
|
||||||
|
if (message.is_user) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the rpg_companion_swipes data for current swipe
|
||||||
|
// Data can be in two places:
|
||||||
|
// 1. message.extra.rpg_companion_swipes (current session, before save)
|
||||||
|
// 2. message.swipe_info[swipeId].extra.rpg_companion_swipes (loaded from file)
|
||||||
|
const currentSwipeId = message.swipe_id || 0;
|
||||||
|
let swipeData = message.extra?.rpg_companion_swipes;
|
||||||
|
|
||||||
|
// If not in message.extra, check swipe_info
|
||||||
|
if (!swipeData && message.swipe_info && message.swipe_info[currentSwipeId]) {
|
||||||
|
swipeData = message.swipe_info[currentSwipeId].extra?.rpg_companion_swipes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!swipeData) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackerData = swipeData[currentSwipeId];
|
||||||
|
if (!trackerData) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the historical tracker data using the shared function
|
||||||
|
const formattedContext = formatHistoricalTrackerData(trackerData, trackerConfig, userName);
|
||||||
|
if (!formattedContext) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the context wrapper
|
||||||
|
const preamble = historyPersistence.contextPreamble || 'Context for that moment:';
|
||||||
|
const wrappedContext = `\n${preamble}\n${formattedContext}`;
|
||||||
|
|
||||||
|
// Determine which message index to store based on injection position
|
||||||
|
let targetIndex = i; // Default: the assistant message itself
|
||||||
|
|
||||||
|
if (position === 'user_message_end') {
|
||||||
|
// Find the preceding user message before this assistant message
|
||||||
|
// This is the user message that prompted this assistant response
|
||||||
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
|
if (chat[j].is_user && !chat[j].is_system) {
|
||||||
|
targetIndex = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no user message found before, skip this one
|
||||||
|
if (targetIndex === i) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For assistant_message_end, extra_user_message, extra_assistant_message:
|
||||||
|
// We inject into the assistant message itself (for now - extra messages handled differently)
|
||||||
|
|
||||||
|
// Store the context keyed by target index
|
||||||
|
// If multiple assistant messages map to the same user message, append
|
||||||
|
if (contextMap.has(targetIndex)) {
|
||||||
|
contextMap.set(targetIndex, contextMap.get(targetIndex) + wrappedContext);
|
||||||
|
} else {
|
||||||
|
contextMap.set(targetIndex, wrappedContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
processedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return contextMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares historical context for injection into prompts.
|
||||||
|
* This builds the context map and stores it for use by prompt event handlers.
|
||||||
|
* Does NOT modify the original chat messages.
|
||||||
|
*/
|
||||||
|
function prepareHistoricalContextInjection() {
|
||||||
|
const historyPersistence = extensionSettings.historyPersistence;
|
||||||
|
if (!historyPersistence || !historyPersistence.enabled) {
|
||||||
|
pendingContextMap = new Map();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSuppressionState || !extensionSettings.enabled) {
|
||||||
|
pendingContextMap = new Map();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context.chat;
|
||||||
|
if (!chat || chat.length < 2) {
|
||||||
|
pendingContextMap = new Map();
|
||||||
|
historyInjectionDone = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build and store the context map for use by prompt handlers
|
||||||
|
pendingContextMap = buildHistoricalContextMap();
|
||||||
|
historyInjectionDone = false; // Reset flag for new generation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the best match position for message content in the prompt.
|
||||||
|
* Tries full content first, then progressively smaller suffixes.
|
||||||
|
*
|
||||||
|
* @param {string} prompt - The prompt to search in
|
||||||
|
* @param {string} messageContent - The message content to find
|
||||||
|
* @returns {{start: number, end: number}|null} - Position info or null if not found
|
||||||
|
*/
|
||||||
|
function findMessageInPrompt(prompt, messageContent) {
|
||||||
|
if (!messageContent || !prompt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find the full content first
|
||||||
|
let searchIndex = prompt.lastIndexOf(messageContent);
|
||||||
|
|
||||||
|
if (searchIndex !== -1) {
|
||||||
|
return { start: searchIndex, end: searchIndex + messageContent.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If full content not found, try last N characters with progressively smaller chunks
|
||||||
|
// This handles cases where messages are truncated in the prompt
|
||||||
|
const searchLengths = [500, 300, 200, 100, 50];
|
||||||
|
|
||||||
|
for (const len of searchLengths) {
|
||||||
|
if (messageContent.length <= len) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchContent = messageContent.slice(-len);
|
||||||
|
searchIndex = prompt.lastIndexOf(searchContent);
|
||||||
|
|
||||||
|
if (searchIndex !== -1) {
|
||||||
|
return { start: searchIndex, end: searchIndex + searchContent.length };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects historical context into a text completion prompt string.
|
||||||
|
* Searches for message content in the prompt and appends context after matches.
|
||||||
|
*
|
||||||
|
* @param {string} prompt - The text completion prompt
|
||||||
|
* @returns {string} - The modified prompt with injected context
|
||||||
|
*/
|
||||||
|
function injectContextIntoTextPrompt(prompt) {
|
||||||
|
if (pendingContextMap.size === 0) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context.chat;
|
||||||
|
let modifiedPrompt = prompt;
|
||||||
|
let injectedCount = 0;
|
||||||
|
|
||||||
|
// Sort by message index descending so we inject from end to start
|
||||||
|
// This prevents position shifts from affecting earlier injections
|
||||||
|
const sortedEntries = Array.from(pendingContextMap.entries()).sort((a, b) => b[0] - a[0]);
|
||||||
|
|
||||||
|
// Process each message that needs context injection
|
||||||
|
for (const [msgIdx, ctxContent] of sortedEntries) {
|
||||||
|
const message = chat[msgIdx];
|
||||||
|
if (!message || typeof message.mes !== 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the message content in the prompt
|
||||||
|
const position = findMessageInPrompt(modifiedPrompt, message.mes);
|
||||||
|
|
||||||
|
if (!position) {
|
||||||
|
// Message not found in prompt (might be truncated or not included)
|
||||||
|
console.debug(`[RPG Companion] Could not find message ${msgIdx} in prompt for context injection`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the context after the message content
|
||||||
|
modifiedPrompt = modifiedPrompt.slice(0, position.end) + ctxContent + modifiedPrompt.slice(position.end);
|
||||||
|
injectedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (injectedCount > 0) {
|
||||||
|
console.log(`[RPG Companion] Injected historical context into ${injectedCount} positions in text prompt`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return modifiedPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects historical context into a chat completion message array.
|
||||||
|
* Modifies the content of messages in the array directly.
|
||||||
|
*
|
||||||
|
* @param {Array} chatMessages - The chat completion message array
|
||||||
|
* @returns {Array} - The modified message array with injected context
|
||||||
|
*/
|
||||||
|
function injectContextIntoChatPrompt(chatMessages) {
|
||||||
|
if (pendingContextMap.size === 0 || !Array.isArray(chatMessages)) {
|
||||||
|
return chatMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context.chat;
|
||||||
|
let injectedCount = 0;
|
||||||
|
|
||||||
|
// Process each message that needs context injection
|
||||||
|
for (const [msgIdx, ctxContent] of pendingContextMap) {
|
||||||
|
const originalMessage = chat[msgIdx];
|
||||||
|
if (!originalMessage || typeof originalMessage.mes !== 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageContent = originalMessage.mes;
|
||||||
|
|
||||||
|
// Find this message in the chat completion array by matching content
|
||||||
|
// Try full content first, then progressively smaller suffixes
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
for (const promptMsg of chatMessages) {
|
||||||
|
if (!promptMsg.content || typeof promptMsg.content !== 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try full content match
|
||||||
|
if (promptMsg.content.includes(messageContent)) {
|
||||||
|
promptMsg.content = promptMsg.content + ctxContent;
|
||||||
|
injectedCount++;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try suffix matches for truncated messages
|
||||||
|
const searchLengths = [500, 300, 200, 100, 50];
|
||||||
|
for (const len of searchLengths) {
|
||||||
|
if (messageContent.length <= len) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchContent = messageContent.slice(-len);
|
||||||
|
if (promptMsg.content.includes(searchContent)) {
|
||||||
|
promptMsg.content = promptMsg.content + ctxContent;
|
||||||
|
injectedCount++;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
console.debug(`[RPG Companion] Could not find message ${msgIdx} in chat prompt for context injection`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (injectedCount > 0) {
|
||||||
|
console.log(`[RPG Companion] Injected historical context into ${injectedCount} messages in chat prompt`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chatMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects historical context into finalMesSend message array (text completion).
|
||||||
|
* Iterates through chat and finalMesSend in order, matching by content to skip injected messages.
|
||||||
|
*
|
||||||
|
* @param {Array} finalMesSend - The array of message objects {message: string, extensionPrompts: []}
|
||||||
|
* @returns {number} - Number of injections made
|
||||||
|
*/
|
||||||
|
function injectContextIntoFinalMesSend(finalMesSend) {
|
||||||
|
if (pendingContextMap.size === 0 || !Array.isArray(finalMesSend) || finalMesSend.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = getContext();
|
||||||
|
const chat = context.chat;
|
||||||
|
if (!chat || chat.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let injectedCount = 0;
|
||||||
|
|
||||||
|
// Build a map from chat index to finalMesSend index by matching content in order
|
||||||
|
// This handles injected messages (author's note, OOC, etc.) that exist in finalMesSend but not in chat
|
||||||
|
const chatToMesSendMap = new Map();
|
||||||
|
let mesSendIdx = 0;
|
||||||
|
|
||||||
|
for (let chatIdx = 0; chatIdx < chat.length && mesSendIdx < finalMesSend.length; chatIdx++) {
|
||||||
|
const chatMsg = chat[chatIdx];
|
||||||
|
if (!chatMsg || chatMsg.is_system) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatContent = chatMsg.mes || '';
|
||||||
|
|
||||||
|
// Look for this chat message in finalMesSend starting from current position
|
||||||
|
// Skip any finalMesSend entries that don't match (they're injected content)
|
||||||
|
while (mesSendIdx < finalMesSend.length) {
|
||||||
|
const mesSendObj = finalMesSend[mesSendIdx];
|
||||||
|
if (!mesSendObj || !mesSendObj.message) {
|
||||||
|
mesSendIdx++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this finalMesSend message contains the chat content
|
||||||
|
// Use a substring match since instruct formatting adds prefixes/suffixes
|
||||||
|
// Match with sufficient content (first 50 chars or full message if shorter)
|
||||||
|
const matchContent = chatContent.length > 50
|
||||||
|
? chatContent.substring(0, 50)
|
||||||
|
: chatContent;
|
||||||
|
|
||||||
|
if (matchContent && mesSendObj.message.includes(matchContent)) {
|
||||||
|
// Found a match - record the mapping
|
||||||
|
chatToMesSendMap.set(chatIdx, mesSendIdx);
|
||||||
|
mesSendIdx++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This finalMesSend entry doesn't match - it's injected content, skip it
|
||||||
|
mesSendIdx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now inject context using the map
|
||||||
|
for (const [chatIdx, ctxContent] of pendingContextMap) {
|
||||||
|
const targetMesSendIdx = chatToMesSendMap.get(chatIdx);
|
||||||
|
|
||||||
|
if (targetMesSendIdx === undefined) {
|
||||||
|
console.debug(`[RPG Companion] Chat message ${chatIdx} not found in finalMesSend mapping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mesSendObj = finalMesSend[targetMesSendIdx];
|
||||||
|
if (!mesSendObj || !mesSendObj.message) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append context to this message
|
||||||
|
mesSendObj.message = mesSendObj.message + ctxContent;
|
||||||
|
injectedCount++;
|
||||||
|
console.debug(`[RPG Companion] Injected context for chat[${chatIdx}] into finalMesSend[${targetMesSendIdx}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return injectedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for GENERATE_BEFORE_COMBINE_PROMPTS (text completion).
|
||||||
|
* Injects historical context into the finalMesSend array before prompt combination.
|
||||||
|
* This is more reliable than post-combine string searching.
|
||||||
|
*
|
||||||
|
* @param {Object} eventData - Event data with finalMesSend and other properties
|
||||||
|
*/
|
||||||
|
function onGenerateBeforeCombinePrompts(eventData) {
|
||||||
|
if (!eventData || !Array.isArray(eventData.finalMesSend)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip for OpenAI (uses chat completion)
|
||||||
|
if (eventData.api === 'openai') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only inject if we have pending context
|
||||||
|
if (pendingContextMap.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const injectedCount = injectContextIntoFinalMesSend(eventData.finalMesSend);
|
||||||
|
if (injectedCount > 0) {
|
||||||
|
console.log(`[RPG Companion] Injected historical context into ${injectedCount} messages in finalMesSend`);
|
||||||
|
historyInjectionDone = true; // Mark as done to prevent double injection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for GENERATE_AFTER_COMBINE_PROMPTS (text completion).
|
||||||
|
* This is now a backup/fallback - primary injection happens in BEFORE_COMBINE.
|
||||||
|
* Also fixes newline spacing after </context> tag.
|
||||||
|
*
|
||||||
|
* @param {Object} eventData - Event data with prompt property
|
||||||
|
*/
|
||||||
|
function onGenerateAfterCombinePrompts(eventData) {
|
||||||
|
if (!eventData || typeof eventData.prompt !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.dryRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let didInjectHistory = false;
|
||||||
|
|
||||||
|
// Inject historical context if available and not already done
|
||||||
|
if (!historyInjectionDone && pendingContextMap.size > 0) {
|
||||||
|
// Fallback injection for edge cases where BEFORE_COMBINE didn't work
|
||||||
|
console.log('[RPG Companion] Using fallback string-based injection (AFTER_COMBINE)');
|
||||||
|
eventData.prompt = injectContextIntoTextPrompt(eventData.prompt);
|
||||||
|
didInjectHistory = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always fix newlines around context tags (whether we just injected or not)
|
||||||
|
eventData.prompt = eventData.prompt.replace(/<context>/g, '\n<context>');
|
||||||
|
eventData.prompt = eventData.prompt.replace(/<\/context>/g, '</context>\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for CHAT_COMPLETION_PROMPT_READY.
|
||||||
|
* Injects historical context into the chat message array.
|
||||||
|
* Also fixes newline spacing around <context> tags.
|
||||||
|
*
|
||||||
|
* @param {Object} eventData - Event data with chat property
|
||||||
|
*/
|
||||||
|
function onChatCompletionPromptReady(eventData) {
|
||||||
|
if (!eventData || !Array.isArray(eventData.chat)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.dryRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject historical context if we have pending context
|
||||||
|
if (pendingContextMap.size > 0) {
|
||||||
|
eventData.chat = injectContextIntoChatPrompt(eventData.chat);
|
||||||
|
// DON'T clear pendingContextMap here - let it persist for other generations
|
||||||
|
// (e.g., prewarm extensions). It will be cleared on GENERATION_ENDED.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix newlines around context tags for all messages
|
||||||
|
for (const message of eventData.chat) {
|
||||||
|
if (message.content && typeof message.content === 'string') {
|
||||||
|
message.content = message.content.replace(/<context>/g, '\n<context>');
|
||||||
|
message.content = message.content.replace(/<\/context>/g, '</context>\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event handler for generation start.
|
* Event handler for generation start.
|
||||||
@@ -55,8 +565,8 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
// console.log('[RPG Companion] Committed Prompt:', committedTrackerData);
|
// console.log('[RPG Companion] Committed Prompt:', committedTrackerData);
|
||||||
|
|
||||||
// Skip tracker injection for image generation requests
|
// Skip tracker injection for image generation requests
|
||||||
if (data?.quietImage) {
|
if (data?.quietImage || data?.quiet_image || data?.isImageGeneration) {
|
||||||
// console.log('[RPG Companion] Detected image generation (quietImage=true), skipping tracker injection');
|
// console.log('[RPG Companion] Detected image generation, skipping tracker injection');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,101 +608,12 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
// Ensure checkpoint is applied before generation
|
// Ensure checkpoint is applied before generation
|
||||||
await restoreCheckpointOnLoad();
|
await restoreCheckpointOnLoad();
|
||||||
|
|
||||||
const currentChatLength = chat ? chat.length : 0;
|
// If this is a new generation (not a swipe and not the tracker update pass),
|
||||||
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
// commit the tracker data from the last assistant message (N-1 rule).
|
||||||
|
// Passing chat.length ensures we start searching backwards from the end of the chat,
|
||||||
// For TOGETHER mode: Commit when user sends message (before first generation)
|
// correctly finding the latest valid assistant state regardless of where the user message is.
|
||||||
if (extensionSettings.generationMode === 'together') {
|
if (!lastActionWasSwipe && !isGenerating) {
|
||||||
// By the time onGenerationStarted fires, ST has already added the placeholder AI message
|
commitTrackerDataFromPriorMessage(chat ? chat.length : 0);
|
||||||
// So we check the second-to-last message to see if user just sent a message
|
|
||||||
const secondToLastMessage = chat && chat.length > 1 ? chat[chat.length - 2] : null;
|
|
||||||
const isUserMessage = secondToLastMessage && secondToLastMessage.is_user;
|
|
||||||
|
|
||||||
// Commit if:
|
|
||||||
// 1. Second-to-last message is from USER (user just sent message)
|
|
||||||
// 2. Not a swipe (lastActionWasSwipe = false)
|
|
||||||
// 3. Haven't already committed for this chat length (prevent streaming duplicates)
|
|
||||||
const shouldCommit = isUserMessage && !lastActionWasSwipe && currentChatLength !== lastCommittedChatLength;
|
|
||||||
|
|
||||||
if (shouldCommit) {
|
|
||||||
// console.log('[RPG Companion] 📝 TOGETHER MODE COMMIT: User sent message - committing data from BEFORE user message');
|
|
||||||
// console.log('[RPG Companion] Chat length:', currentChatLength, 'Last committed:', lastCommittedChatLength);
|
|
||||||
// console.log('[RPG Companion] BEFORE: committedTrackerData =', {
|
|
||||||
// userStats: committedTrackerData.userStats ? `${committedTrackerData.userStats.substring(0, 50)}...` : 'null',
|
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? `${committedTrackerData.characterThoughts.substring(0, 100)}...` : 'null'
|
|
||||||
// // });
|
|
||||||
// console.log('[RPG Companion] BEFORE: lastGeneratedData =', {
|
|
||||||
// userStats: lastGeneratedData.userStats ? `${lastGeneratedData.userStats.substring(0, 50)}...` : 'null',
|
|
||||||
// infoBox: lastGeneratedData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: lastGeneratedData.characterThoughts ? `${lastGeneratedData.characterThoughts.substring(0, 100)}...` : 'null'
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Commit displayed data (from before user sent message)
|
|
||||||
committedTrackerData.userStats = lastGeneratedData.userStats;
|
|
||||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
|
||||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
|
||||||
|
|
||||||
// Track chat length to prevent duplicate commits
|
|
||||||
lastCommittedChatLength = currentChatLength;
|
|
||||||
|
|
||||||
// console.log('[RPG Companion] AFTER: committedTrackerData =', {
|
|
||||||
// userStats: committedTrackerData.userStats ? `${committedTrackerData.userStats.substring(0, 50)}...` : 'null',
|
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? `${committedTrackerData.characterThoughts.substring(0, 100)}...` : 'null'
|
|
||||||
// });
|
|
||||||
} else if (lastActionWasSwipe) {
|
|
||||||
// console.log('[RPG Companion] ⏭️ Skipping commit: swipe (using previous committed data)');
|
|
||||||
} else if (!isUserMessage) {
|
|
||||||
// console.log('[RPG Companion] ⏭️ Skipping commit: second-to-last message is not user message (likely swipe or continuation)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('[RPG Companion] 📦 TOGETHER MODE: Injecting committed tracker data into prompt');
|
|
||||||
// console.log('[RPG Companion] committedTrackerData =', {
|
|
||||||
// userStats: committedTrackerData.userStats ? `${committedTrackerData.userStats.substring(0, 50)}...` : 'null',
|
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? `${committedTrackerData.characterThoughts.substring(0, 100)}...` : 'null'
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
// For SEPARATE and EXTERNAL modes: Check if we need to commit extension data
|
|
||||||
// BUT: Only do this for the MAIN generation, not the tracker update generation
|
|
||||||
// If isGenerating is true, this is the tracker update generation (second call), so skip flag logic
|
|
||||||
// console.log('[RPG Companion DEBUG] Before generating:', lastGeneratedData.characterThoughts, ' , committed - ', committedTrackerData.characterThoughts);
|
|
||||||
if ((extensionSettings.generationMode === 'separate' || extensionSettings.generationMode === 'external') && !isGenerating) {
|
|
||||||
if (!lastActionWasSwipe) {
|
|
||||||
// User sent a new message - commit lastGeneratedData before generation
|
|
||||||
// console.log('[RPG Companion] 📝 COMMIT: New message - committing lastGeneratedData');
|
|
||||||
// console.log('[RPG Companion] BEFORE commit - committedTrackerData:', {
|
|
||||||
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
|
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null'
|
|
||||||
// // });
|
|
||||||
// console.log('[RPG Companion] BEFORE commit - lastGeneratedData:', {
|
|
||||||
// userStats: lastGeneratedData.userStats ? 'exists' : 'null',
|
|
||||||
// infoBox: lastGeneratedData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: lastGeneratedData.characterThoughts ? 'exists' : 'null'
|
|
||||||
// });
|
|
||||||
committedTrackerData.userStats = lastGeneratedData.userStats;
|
|
||||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
|
||||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
|
||||||
// console.log('[RPG Companion] AFTER commit - committedTrackerData:', {
|
|
||||||
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
|
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null'
|
|
||||||
// });
|
|
||||||
|
|
||||||
// Reset flag after committing (ready for next cycle)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// console.log('[RPG Companion] 🔄 SWIPE: Using existing committedTrackerData (no commit)');
|
|
||||||
// console.log('[RPG Companion] committedTrackerData:', {
|
|
||||||
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
|
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
|
||||||
// characterThoughts: committedTrackerData.characterThoughts ? 'exists' : 'null'
|
|
||||||
// });
|
|
||||||
// Reset flag after using it (swipe generation complete, ready for next action)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the committed tracker data as source for generation
|
// Use the committed tracker data as source for generation
|
||||||
@@ -261,7 +682,7 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
if (extensionSettings.enableHtmlPrompt && !shouldSuppress) {
|
if (extensionSettings.enableHtmlPrompt && !shouldSuppress) {
|
||||||
// Use custom HTML prompt if set, otherwise use default
|
// Use custom HTML prompt if set, otherwise use default
|
||||||
const htmlPromptText = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT;
|
const htmlPromptText = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT;
|
||||||
const htmlPrompt = `\n${htmlPromptText}`;
|
const htmlPrompt = `\n- ${htmlPromptText}\n`;
|
||||||
|
|
||||||
setExtensionPrompt('rpg-companion-html', htmlPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-html', htmlPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||||
// console.log('[RPG Companion] Injected HTML prompt at depth 0 for together mode');
|
// console.log('[RPG Companion] Injected HTML prompt at depth 0 for together mode');
|
||||||
@@ -274,7 +695,7 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
if (extensionSettings.enableDialogueColoring && !shouldSuppress) {
|
if (extensionSettings.enableDialogueColoring && !shouldSuppress) {
|
||||||
// Use custom Dialogue Coloring prompt if set, otherwise use default
|
// Use custom Dialogue Coloring prompt if set, otherwise use default
|
||||||
const dialogueColoringPromptText = extensionSettings.customDialogueColoringPrompt || DEFAULT_DIALOGUE_COLORING_PROMPT;
|
const dialogueColoringPromptText = extensionSettings.customDialogueColoringPrompt || DEFAULT_DIALOGUE_COLORING_PROMPT;
|
||||||
const dialogueColoringPrompt = `\n${dialogueColoringPromptText}`;
|
const dialogueColoringPrompt = `\n- ${dialogueColoringPromptText}\n`;
|
||||||
|
|
||||||
setExtensionPrompt('rpg-companion-dialogue-coloring', dialogueColoringPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-dialogue-coloring', dialogueColoringPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||||
// console.log('[RPG Companion] Injected Dialogue Coloring prompt at depth 0 for together mode');
|
// console.log('[RPG Companion] Injected Dialogue Coloring prompt at depth 0 for together mode');
|
||||||
@@ -283,11 +704,37 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
setExtensionPrompt('rpg-companion-dialogue-coloring', '', extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-dialogue-coloring', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject Deception System prompt separately at depth 0 if enabled
|
||||||
|
if (extensionSettings.enableDeceptionSystem && !shouldSuppress) {
|
||||||
|
// Use custom Deception prompt if set, otherwise use default
|
||||||
|
const deceptionPromptText = extensionSettings.customDeceptionPrompt || DEFAULT_DECEPTION_PROMPT;
|
||||||
|
const deceptionPrompt = `\n- ${deceptionPromptText}\n`;
|
||||||
|
|
||||||
|
setExtensionPrompt('rpg-companion-deception', deceptionPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
// console.log('[RPG Companion] Injected Deception System prompt at depth 0 for together mode');
|
||||||
|
} else {
|
||||||
|
// Clear Deception System prompt if disabled
|
||||||
|
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject Omniscience Filter prompt separately at depth 0 if enabled
|
||||||
|
if (extensionSettings.enableOmniscienceFilter && !shouldSuppress) {
|
||||||
|
// Use custom Omniscience Filter prompt if set, otherwise use default
|
||||||
|
const omnisciencePromptText = extensionSettings.customOmnisciencePrompt || DEFAULT_OMNISCIENCE_FILTER_PROMPT;
|
||||||
|
const omnisciencePrompt = `\n${omnisciencePromptText}\n`;
|
||||||
|
|
||||||
|
setExtensionPrompt('rpg-companion-omniscience', omnisciencePrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
// console.log('[RPG Companion] Injected Omniscience Filter prompt at depth 0 for together mode');
|
||||||
|
} else {
|
||||||
|
// Clear Omniscience Filter prompt if disabled
|
||||||
|
setExtensionPrompt('rpg-companion-omniscience', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
// Inject Spotify prompt separately at depth 0 if enabled
|
// Inject Spotify prompt separately at depth 0 if enabled
|
||||||
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
|
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
|
||||||
// Use custom Spotify prompt if set, otherwise use default
|
// Use custom Spotify prompt if set, otherwise use default
|
||||||
const spotifyPromptText = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
|
const spotifyPromptText = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
|
||||||
const spotifyPrompt = `\n${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}`;
|
const spotifyPrompt = `\n- ${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}\n`;
|
||||||
|
|
||||||
setExtensionPrompt('rpg-companion-spotify', spotifyPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-spotify', spotifyPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||||
// console.log('[RPG Companion] Injected Spotify prompt at depth 0 for together mode');
|
// console.log('[RPG Companion] Injected Spotify prompt at depth 0 for together mode');
|
||||||
@@ -295,17 +742,33 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
// Clear Spotify prompt if disabled
|
// Clear Spotify prompt if disabled
|
||||||
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject CYOA prompt separately at depth 0 if enabled (injected last to appear last in prompt)
|
||||||
|
if (extensionSettings.enableCYOA && !shouldSuppress) {
|
||||||
|
// Use custom CYOA prompt if set, otherwise use default
|
||||||
|
const cyoaPromptText = extensionSettings.customCYOAPrompt || DEFAULT_CYOA_PROMPT;
|
||||||
|
const cyoaPrompt = `\n- ${cyoaPromptText}\n`;
|
||||||
|
|
||||||
|
setExtensionPrompt('rpg-companion-zzz-cyoa', cyoaPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
// console.log('[RPG Companion] Injected CYOA prompt at depth 0 for together mode');
|
||||||
|
} else {
|
||||||
|
// Clear CYOA prompt if disabled
|
||||||
|
setExtensionPrompt('rpg-companion-zzz-cyoa', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
} else if (extensionSettings.generationMode === 'separate' || extensionSettings.generationMode === 'external') {
|
} else if (extensionSettings.generationMode === 'separate' || extensionSettings.generationMode === 'external') {
|
||||||
// In SEPARATE and EXTERNAL modes, inject the contextual summary for main roleplay generation
|
// In SEPARATE and EXTERNAL modes, inject the contextual summary for main roleplay generation
|
||||||
const contextSummary = generateContextualSummary();
|
const contextSummary = generateContextualSummary();
|
||||||
|
|
||||||
if (contextSummary) {
|
if (contextSummary) {
|
||||||
const wrappedContext = `\nHere is context information about the current scene, and what follows is the last message in the chat history:
|
// Use custom context instructions prompt if set, otherwise use default
|
||||||
|
const contextInstructionsText = extensionSettings.customContextInstructionsPrompt || DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT;
|
||||||
|
|
||||||
|
const wrappedContext = `
|
||||||
<context>
|
<context>
|
||||||
${contextSummary}
|
${contextSummary}
|
||||||
|
${contextInstructionsText}
|
||||||
Ensure these details naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting performance, low hygiene influencing social interactions, environmental factors shaping the scene, or a character's emotional state coloring their responses.
|
</context>`;
|
||||||
</context>\n\n`;
|
|
||||||
|
|
||||||
// Inject context at depth 1 (before last user message) as SYSTEM
|
// Inject context at depth 1 (before last user message) as SYSTEM
|
||||||
// Skip when a guided generation injection is present to avoid conflicting instructions
|
// Skip when a guided generation injection is present to avoid conflicting instructions
|
||||||
@@ -322,7 +785,7 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
|||||||
if (extensionSettings.enableHtmlPrompt && !shouldSuppress) {
|
if (extensionSettings.enableHtmlPrompt && !shouldSuppress) {
|
||||||
// Use custom HTML prompt if set, otherwise use default
|
// Use custom HTML prompt if set, otherwise use default
|
||||||
const htmlPromptText = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT;
|
const htmlPromptText = extensionSettings.customHtmlPrompt || DEFAULT_HTML_PROMPT;
|
||||||
const htmlPrompt = `\n${htmlPromptText}`;
|
const htmlPrompt = `\n- ${htmlPromptText}\n`;
|
||||||
|
|
||||||
setExtensionPrompt('rpg-companion-html', htmlPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-html', htmlPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||||
// console.log('[RPG Companion] Injected HTML prompt at depth 0 for separate/external mode');
|
// console.log('[RPG Companion] Injected HTML prompt at depth 0 for separate/external mode');
|
||||||
@@ -331,11 +794,50 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
|||||||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject Dialogue Coloring prompt separately at depth 0 if enabled
|
||||||
|
if (extensionSettings.enableDialogueColoring && !shouldSuppress) {
|
||||||
|
// Use custom Dialogue Coloring prompt if set, otherwise use default
|
||||||
|
const dialogueColoringPromptText = extensionSettings.customDialogueColoringPrompt || DEFAULT_DIALOGUE_COLORING_PROMPT;
|
||||||
|
const dialogueColoringPrompt = `\n- ${dialogueColoringPromptText}\n`;
|
||||||
|
|
||||||
|
setExtensionPrompt('rpg-companion-dialogue-coloring', dialogueColoringPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
// console.log('[RPG Companion] Injected Dialogue Coloring prompt at depth 0 for separate/external mode');
|
||||||
|
} else {
|
||||||
|
// Clear Dialogue Coloring prompt if disabled
|
||||||
|
setExtensionPrompt('rpg-companion-dialogue-coloring', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject Deception System prompt separately at depth 0 if enabled
|
||||||
|
if (extensionSettings.enableDeceptionSystem && !shouldSuppress) {
|
||||||
|
// Use custom Deception prompt if set, otherwise use default
|
||||||
|
const deceptionPromptText = extensionSettings.customDeceptionPrompt || DEFAULT_DECEPTION_PROMPT;
|
||||||
|
const deceptionPrompt = `\n- ${deceptionPromptText}\n`;
|
||||||
|
|
||||||
|
setExtensionPrompt('rpg-companion-deception', deceptionPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
// console.log('[RPG Companion] Injected Deception System prompt at depth 0 for separate/external mode');
|
||||||
|
} else {
|
||||||
|
// Clear Deception System prompt if disabled
|
||||||
|
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject Omniscience Filter prompt separately at depth 0 if enabled
|
||||||
|
if (extensionSettings.enableOmniscienceFilter && !shouldSuppress) {
|
||||||
|
// Use custom Omniscience Filter prompt if set, otherwise use default
|
||||||
|
const omnisciencePromptText = extensionSettings.customOmnisciencePrompt || DEFAULT_OMNISCIENCE_FILTER_PROMPT;
|
||||||
|
const omnisciencePrompt = `\n${omnisciencePromptText}\n`;
|
||||||
|
|
||||||
|
setExtensionPrompt('rpg-companion-omniscience', omnisciencePrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
// console.log('[RPG Companion] Injected Omniscience Filter prompt at depth 0 for separate/external mode');
|
||||||
|
} else {
|
||||||
|
// Clear Omniscience Filter prompt if disabled
|
||||||
|
setExtensionPrompt('rpg-companion-omniscience', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
// Inject Spotify prompt separately at depth 0 if enabled
|
// Inject Spotify prompt separately at depth 0 if enabled
|
||||||
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
|
if (extensionSettings.enableSpotifyMusic && !shouldSuppress) {
|
||||||
// Use custom Spotify prompt if set, otherwise use default
|
// Use custom Spotify prompt if set, otherwise use default
|
||||||
const spotifyPromptText = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
|
const spotifyPromptText = extensionSettings.customSpotifyPrompt || DEFAULT_SPOTIFY_PROMPT;
|
||||||
const spotifyPrompt = `\n${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}`;
|
const spotifyPrompt = `\n- ${spotifyPromptText} ${SPOTIFY_FORMAT_INSTRUCTION}\n`;
|
||||||
|
|
||||||
setExtensionPrompt('rpg-companion-spotify', spotifyPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-spotify', spotifyPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||||
// console.log('[RPG Companion] Injected Spotify prompt at depth 0 for separate/external mode');
|
// console.log('[RPG Companion] Injected Spotify prompt at depth 0 for separate/external mode');
|
||||||
@@ -344,6 +846,19 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
|||||||
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject CYOA prompt separately at depth 0 if enabled (injected last to appear last in prompt)
|
||||||
|
if (extensionSettings.enableCYOA && !shouldSuppress) {
|
||||||
|
// Use custom CYOA prompt if set, otherwise use default
|
||||||
|
const cyoaPromptText = extensionSettings.customCYOAPrompt || DEFAULT_CYOA_PROMPT;
|
||||||
|
const cyoaPrompt = `\n- ${cyoaPromptText}\n`;
|
||||||
|
|
||||||
|
setExtensionPrompt('rpg-companion-zzz-cyoa', cyoaPrompt, extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
// console.log('[RPG Companion] Injected CYOA prompt at depth 0 for separate/external mode');
|
||||||
|
} else {
|
||||||
|
// Clear CYOA prompt if disabled
|
||||||
|
setExtensionPrompt('rpg-companion-zzz-cyoa', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear together mode injections
|
// Clear together mode injections
|
||||||
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-inject', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
@@ -353,6 +868,40 @@ Ensure these details naturally reflect and influence the narrative. Character be
|
|||||||
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-example', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
|
setExtensionPrompt('rpg-companion-context', '', extension_prompt_types.IN_CHAT, 1, false);
|
||||||
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-html', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
setExtensionPrompt('rpg-companion-dialogue-coloring', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
setExtensionPrompt('rpg-companion-deception', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
setExtensionPrompt('rpg-companion-omniscience', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
|
setExtensionPrompt('rpg-companion-zzz-cyoa', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
setExtensionPrompt('rpg-companion-spotify', '', extension_prompt_types.IN_CHAT, 0, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set suppression state for the historical context injection
|
||||||
|
currentSuppressionState = shouldSuppress;
|
||||||
|
|
||||||
|
// Prepare historical context for injection into prompts
|
||||||
|
// This builds the context map but does NOT modify original chat messages
|
||||||
|
// The persistent event listeners will inject it into all prompts until cleared
|
||||||
|
prepareHistoricalContextInjection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the history injection event listeners.
|
||||||
|
* These are persistent listeners that inject context into ALL generations
|
||||||
|
* while pendingContextMap has data. Should be called once at extension init.
|
||||||
|
*/
|
||||||
|
export function initHistoryInjectionListeners() {
|
||||||
|
// Register persistent listeners for prompt injection
|
||||||
|
// These check pendingContextMap and only inject if there's data
|
||||||
|
|
||||||
|
// Primary: BEFORE_COMBINE for text completion (more reliable - modifies message objects)
|
||||||
|
eventSource.on(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, onGenerateBeforeCombinePrompts);
|
||||||
|
|
||||||
|
// Fallback: AFTER_COMBINE for text completion (string-based injection)
|
||||||
|
eventSource.on(event_types.GENERATE_AFTER_COMBINE_PROMPTS, onGenerateAfterCombinePrompts);
|
||||||
|
|
||||||
|
// Chat completion (OpenAI, etc.)
|
||||||
|
eventSource.on(event_types.CHAT_COMPLETION_PROMPT_READY, onChatCompletionPromptReady);
|
||||||
|
|
||||||
|
console.log('[RPG Companion] History injection listeners initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
import { extensionSettings, committedTrackerData } from '../../core/state.js';
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
|
import { getWeatherKeywordsAsPromptString } from '../ui/weatherEffects.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a field name to snake_case for use as JSON key
|
* Converts a field name to snake_case for use as JSON key
|
||||||
@@ -15,10 +17,23 @@ import { getContext } from '../../../../../../extensions.js';
|
|||||||
function toSnakeCase(name) {
|
function toSnakeCase(name) {
|
||||||
return name
|
return name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9]+/g, '_')
|
.replace(/[^\p{L}\p{N}]+/gu, '_')
|
||||||
.replace(/^_+|_+$/g, '');
|
.replace(/^_+|_+$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
|
||||||
|
* Parenthetical content is treated as a description/hint, not part of the key.
|
||||||
|
* Example: "Conditions (up to 5 traits)" -> "conditions"
|
||||||
|
* Example: "Status Effects" -> "status_effects"
|
||||||
|
* @param {string} name - Field name, possibly with parenthetical description
|
||||||
|
* @returns {string} snake_case key from the base name only
|
||||||
|
*/
|
||||||
|
function toFieldKey(name) {
|
||||||
|
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
|
||||||
|
return toSnakeCase(baseName);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds User Stats JSON format instruction
|
* Builds User Stats JSON format instruction
|
||||||
* @returns {string} JSON format instruction for user stats
|
* @returns {string} JSON format instruction for user stats
|
||||||
@@ -28,6 +43,7 @@ export function buildUserStatsJSONInstruction() {
|
|||||||
const trackerConfig = extensionSettings.trackerConfig;
|
const trackerConfig = extensionSettings.trackerConfig;
|
||||||
const userStatsConfig = trackerConfig?.userStats;
|
const userStatsConfig = trackerConfig?.userStats;
|
||||||
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
const enabledStats = userStatsConfig?.customStats?.filter(s => s && s.enabled && s.name) || [];
|
||||||
|
const displayMode = userStatsConfig?.statsDisplayMode || 'percentage';
|
||||||
|
|
||||||
let instruction = '{\n';
|
let instruction = '{\n';
|
||||||
instruction += ' "stats": [\n';
|
instruction += ' "stats": [\n';
|
||||||
@@ -36,7 +52,12 @@ export function buildUserStatsJSONInstruction() {
|
|||||||
for (let i = 0; i < enabledStats.length; i++) {
|
for (let i = 0; i < enabledStats.length; i++) {
|
||||||
const stat = enabledStats[i];
|
const stat = enabledStats[i];
|
||||||
const comma = i < enabledStats.length - 1 ? ',' : '';
|
const comma = i < enabledStats.length - 1 ? ',' : '';
|
||||||
instruction += ` {"id": "${stat.id}", "name": "${stat.name}", "value": X}${comma}\n`;
|
if (displayMode === 'number') {
|
||||||
|
const maxValue = stat.maxValue || 100;
|
||||||
|
instruction += ` {"id": "${stat.id}", "name": "${stat.name}", "value": X}${comma} // 0 to ${maxValue}\n`;
|
||||||
|
} else {
|
||||||
|
instruction += ` {"id": "${stat.id}", "name": "${stat.name}", "value": X}${comma} // 0 to 100 (percentage)\n`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
instruction += ' ],\n';
|
instruction += ' ],\n';
|
||||||
@@ -45,9 +66,24 @@ export function buildUserStatsJSONInstruction() {
|
|||||||
if (userStatsConfig?.statusSection?.enabled) {
|
if (userStatsConfig?.statusSection?.enabled) {
|
||||||
instruction += ' "status": {\n';
|
instruction += ' "status": {\n';
|
||||||
if (userStatsConfig.statusSection.showMoodEmoji) {
|
if (userStatsConfig.statusSection.showMoodEmoji) {
|
||||||
instruction += ' "mood": "Mood Emoji",\n';
|
instruction += ' "mood": "Mood Emoji"';
|
||||||
|
}
|
||||||
|
// Add all custom status fields
|
||||||
|
const customFields = userStatsConfig.statusSection.customFields || [];
|
||||||
|
if (customFields.length > 0) {
|
||||||
|
for (let i = 0; i < customFields.length; i++) {
|
||||||
|
const fieldName = customFields[i].toLowerCase();
|
||||||
|
const fieldKey = toFieldKey(fieldName);
|
||||||
|
const comma = (i === customFields.length - 1 && !userStatsConfig.statusSection.showMoodEmoji) ? '' : (userStatsConfig.statusSection.showMoodEmoji || i < customFields.length - 1 ? ',\n' : '\n');
|
||||||
|
if (i === 0 && userStatsConfig.statusSection.showMoodEmoji) {
|
||||||
|
instruction += ',\n';
|
||||||
|
}
|
||||||
|
instruction += ` "${fieldKey}": "[${fieldName}]"${comma}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!userStatsConfig.statusSection.showMoodEmoji && customFields.length > 0) {
|
||||||
|
instruction += '\n';
|
||||||
}
|
}
|
||||||
instruction += ' "conditions": "[Condition1, Condition2]"\n';
|
|
||||||
instruction += ' },\n';
|
instruction += ' },\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,12 +141,16 @@ export function buildInfoBoxJSONInstruction() {
|
|||||||
let hasFields = false;
|
let hasFields = false;
|
||||||
|
|
||||||
if (widgets.date?.enabled) {
|
if (widgets.date?.enabled) {
|
||||||
instruction += ' "date": {"value": "Weekday, Month, Year"}';
|
const dateFormat = widgets.date.format || 'Weekday, Month, Year';
|
||||||
|
instruction += ` "date": {"value": "${dateFormat}"}`;
|
||||||
hasFields = true;
|
hasFields = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widgets.weather?.enabled) {
|
if (widgets.weather?.enabled) {
|
||||||
instruction += (hasFields ? ',\n' : '') + ' "weather": {"emoji": "Weather Emoji", "forecast": "Forecast"}';
|
// Get valid weather keywords for the current language to guide LLM generation
|
||||||
|
const currentLang = i18n.currentLanguage || 'en';
|
||||||
|
const weatherHint = getWeatherKeywordsAsPromptString(currentLang);
|
||||||
|
instruction += (hasFields ? ',\n' : '') + ` "weather": {"emoji": "Weather Emoji", "forecast": "Forecast"} // ${weatherHint}`;
|
||||||
hasFields = true;
|
hasFields = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,16 +98,19 @@ function applyUserStatsLocks(data, lockedItems) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lock inventory items - handle bracket notation paths like "inventory.onPerson[0]"
|
// Lock inventory items - match by item name instead of index
|
||||||
if (data.inventory && lockedItems.inventory) {
|
if (data.inventory && lockedItems.inventory) {
|
||||||
// Helper function to parse bracket notation and apply lock
|
// Helper function to apply locks based on item name
|
||||||
const applyInventoryLocks = (items, category) => {
|
const applyInventoryLocks = (items, category) => {
|
||||||
if (!Array.isArray(items)) return items;
|
if (!Array.isArray(items)) return items;
|
||||||
|
if (!lockedItems.inventory[category]) return items;
|
||||||
|
|
||||||
return items.map((item, index) => {
|
return items.map((item) => {
|
||||||
// Check if this specific item is locked using bracket notation with inventory prefix
|
// Get item name (handle both string and object formats)
|
||||||
const bracketPath = `${category}[${index}]`;
|
const itemName = typeof item === 'string' ? item : (item.item || item.name || '');
|
||||||
if (lockedItems.inventory[bracketPath]) {
|
|
||||||
|
// Check if this specific item name is locked
|
||||||
|
if (lockedItems.inventory[category][itemName]) {
|
||||||
return typeof item === 'string'
|
return typeof item === 'string'
|
||||||
? { item, locked: true }
|
? { item, locked: true }
|
||||||
: { ...item, locked: true };
|
: { ...item, locked: true };
|
||||||
@@ -131,13 +134,13 @@ function applyUserStatsLocks(data, lockedItems) {
|
|||||||
data.inventory.assets = applyInventoryLocks(data.inventory.assets, 'assets');
|
data.inventory.assets = applyInventoryLocks(data.inventory.assets, 'assets');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply locks to stored items (nested structure with inventory.stored.location[index])
|
// Apply locks to stored items - match by item name
|
||||||
if (data.inventory.stored && lockedItems.inventory.stored) {
|
if (data.inventory.stored && lockedItems.inventory.stored) {
|
||||||
for (const location in data.inventory.stored) {
|
for (const location in data.inventory.stored) {
|
||||||
if (Array.isArray(data.inventory.stored[location])) {
|
if (Array.isArray(data.inventory.stored[location]) && lockedItems.inventory.stored[location]) {
|
||||||
data.inventory.stored[location] = data.inventory.stored[location].map((item, index) => {
|
data.inventory.stored[location] = data.inventory.stored[location].map((item) => {
|
||||||
const bracketPath = `${location}[${index}]`;
|
const itemName = typeof item === 'string' ? item : (item.item || item.name || '');
|
||||||
if (lockedItems.inventory.stored[bracketPath]) {
|
if (lockedItems.inventory.stored[location][itemName]) {
|
||||||
return typeof item === 'string'
|
return typeof item === 'string'
|
||||||
? { item, locked: true }
|
? { item, locked: true }
|
||||||
: { ...item, locked: true };
|
: { ...item, locked: true };
|
||||||
@@ -248,7 +251,7 @@ function applyCharactersLocks(data, lockedItems) {
|
|||||||
// Use the same conversion as toSnakeCase in thoughts.js
|
// Use the same conversion as toSnakeCase in thoughts.js
|
||||||
const snakeCaseFieldName = fieldName
|
const snakeCaseFieldName = fieldName
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9]+/g, '_')
|
.replace(/[^\p{L}\p{N}]+/gu, '_')
|
||||||
.replace(/^_+|_+$/g, '');
|
.replace(/^_+|_+$/g, '');
|
||||||
|
|
||||||
let locked = false;
|
let locked = false;
|
||||||
|
|||||||
@@ -7,7 +7,57 @@
|
|||||||
import { extensionSettings, FEATURE_FLAGS, addDebugLog } from '../../core/state.js';
|
import { extensionSettings, FEATURE_FLAGS, addDebugLog } from '../../core/state.js';
|
||||||
import { saveSettings } from '../../core/persistence.js';
|
import { saveSettings } from '../../core/persistence.js';
|
||||||
import { extractInventory } from './inventoryParser.js';
|
import { extractInventory } from './inventoryParser.js';
|
||||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
import { repairJSON, extractJSONFromText } from '../../utils/jsonRepair.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unwraps common envelope keys models may use around tracker payloads.
|
||||||
|
* Keeps extraction resilient when output is nested under wrappers like "trackers".
|
||||||
|
*
|
||||||
|
* @param {object} payload - Parsed JSON payload
|
||||||
|
* @returns {object} Unwrapped payload (or original when no wrapper exists)
|
||||||
|
*/
|
||||||
|
function unwrapTrackerEnvelope(payload) {
|
||||||
|
let current = payload;
|
||||||
|
|
||||||
|
for (let depth = 0; depth < 4; depth++) {
|
||||||
|
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
current.userStats ||
|
||||||
|
current.infoBox ||
|
||||||
|
current.characters ||
|
||||||
|
current.characterThoughts ||
|
||||||
|
current.presentCharacters
|
||||||
|
) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = current.trackers || current.tracker || current.context || current.state || null;
|
||||||
|
if (!next || typeof next !== 'object') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the base name (before parentheses) and converts to snake_case for use as JSON key.
|
||||||
|
* Example: "Conditions (up to 5 traits)" -> "conditions"
|
||||||
|
* @param {string} name - Field name, possibly with parenthetical description
|
||||||
|
* @returns {string} snake_case key from the base name only
|
||||||
|
*/
|
||||||
|
function toFieldKey(name) {
|
||||||
|
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
|
||||||
|
return baseName
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\p{L}\p{N}]+/gu, '_')
|
||||||
|
.replace(/^_+|_+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to separate emoji from text in a string
|
* Helper to separate emoji from text in a string
|
||||||
@@ -141,9 +191,12 @@ function debugLog(message, data = null) {
|
|||||||
* Handles both separate code blocks and combined code blocks gracefully.
|
* Handles both separate code blocks and combined code blocks gracefully.
|
||||||
*
|
*
|
||||||
* @param {string} responseText - The raw AI response text
|
* @param {string} responseText - The raw AI response text
|
||||||
|
* @param {Object} [options] - Parser behavior options
|
||||||
|
* @param {boolean} [options.suppressNoDataError=false] - Avoid console error when no tracker data is found
|
||||||
* @returns {{userStats: string|null, infoBox: string|null, characterThoughts: string|null}} Parsed tracker data
|
* @returns {{userStats: string|null, infoBox: string|null, characterThoughts: string|null}} Parsed tracker data
|
||||||
*/
|
*/
|
||||||
export function parseResponse(responseText) {
|
export function parseResponse(responseText, options = {}) {
|
||||||
|
const { suppressNoDataError = false } = options;
|
||||||
const result = {
|
const result = {
|
||||||
userStats: null,
|
userStats: null,
|
||||||
infoBox: null,
|
infoBox: null,
|
||||||
@@ -198,7 +251,9 @@ export function parseResponse(responseText) {
|
|||||||
if (depth === 0) {
|
if (depth === 0) {
|
||||||
// Found complete JSON object
|
// Found complete JSON object
|
||||||
const jsonContent = cleanedResponse.substring(i, j).trim();
|
const jsonContent = cleanedResponse.substring(i, j).trim();
|
||||||
extractedObjects.push(jsonContent);
|
if (jsonContent) {
|
||||||
|
extractedObjects.push(jsonContent);
|
||||||
|
}
|
||||||
i = j;
|
i = j;
|
||||||
} else {
|
} else {
|
||||||
i++;
|
i++;
|
||||||
@@ -213,32 +268,40 @@ export function parseResponse(responseText) {
|
|||||||
debugLog(`[RPG Parser] ✓ Found ${extractedObjects.length} raw JSON objects (v3 format)`);
|
debugLog(`[RPG Parser] ✓ Found ${extractedObjects.length} raw JSON objects (v3 format)`);
|
||||||
|
|
||||||
// First, try to parse as unified JSON structure (new v3.1 format)
|
// First, try to parse as unified JSON structure (new v3.1 format)
|
||||||
if (extractedObjects.length === 1) {
|
// Look through all extracted objects for unified structure
|
||||||
const parsed = repairJSON(extractedObjects[0]);
|
let foundUnified = false;
|
||||||
if (parsed && (parsed.userStats || parsed.infoBox || parsed.characters)) {
|
for (let idx = 0; idx < extractedObjects.length; idx++) {
|
||||||
|
const parsed = repairJSON(extractedObjects[idx]);
|
||||||
|
const unwrapped = parsed ? unwrapTrackerEnvelope(parsed) : null;
|
||||||
|
if (unwrapped && (unwrapped.userStats || unwrapped.infoBox || unwrapped.characters || unwrapped.characterThoughts || unwrapped.presentCharacters)) {
|
||||||
// console.log('[RPG Parser] ✓ Detected unified JSON structure (v3.1 format)');
|
// console.log('[RPG Parser] ✓ Detected unified JSON structure (v3.1 format)');
|
||||||
|
|
||||||
if (parsed.userStats) {
|
if (unwrapped.userStats) {
|
||||||
result.userStats = JSON.stringify(parsed.userStats);
|
result.userStats = JSON.stringify(unwrapped.userStats);
|
||||||
// console.log('[RPG Parser] ✓ Extracted userStats from unified structure');
|
// console.log('[RPG Parser] ✓ Extracted userStats from unified structure');
|
||||||
}
|
}
|
||||||
if (parsed.infoBox) {
|
if (unwrapped.infoBox) {
|
||||||
result.infoBox = JSON.stringify(parsed.infoBox);
|
result.infoBox = JSON.stringify(unwrapped.infoBox);
|
||||||
// console.log('[RPG Parser] ✓ Extracted infoBox from unified structure');
|
// console.log('[RPG Parser] ✓ Extracted infoBox from unified structure');
|
||||||
}
|
}
|
||||||
if (parsed.characters) {
|
const unifiedCharacters = unwrapped.characters || unwrapped.presentCharacters || unwrapped.characterThoughts;
|
||||||
result.characterThoughts = JSON.stringify(parsed.characters);
|
if (unifiedCharacters) {
|
||||||
|
result.characterThoughts = JSON.stringify(unifiedCharacters);
|
||||||
// console.log('[RPG Parser] ✓ Extracted characters from unified structure');
|
// console.log('[RPG Parser] ✓ Extracted characters from unified structure');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.userStats || result.infoBox || result.characterThoughts) {
|
foundUnified = true;
|
||||||
// console.log('[RPG Parser] ✓ Returning unified JSON parse results');
|
break; // Found unified structure, stop searching
|
||||||
debugLog('[RPG Parser] Returning unified JSON parse results');
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (foundUnified) {
|
||||||
|
// console.log('[RPG Parser] ✓ Returning unified JSON parse results');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no unified structure found, proceed to multi-object classification
|
||||||
|
|
||||||
// Fall back to parsing multiple separate JSON objects (legacy v3.0 format)
|
// Fall back to parsing multiple separate JSON objects (legacy v3.0 format)
|
||||||
for (let idx = 0; idx < extractedObjects.length; idx++) {
|
for (let idx = 0; idx < extractedObjects.length; idx++) {
|
||||||
const jsonContent = extractedObjects[idx];
|
const jsonContent = extractedObjects[idx];
|
||||||
@@ -248,11 +311,12 @@ export function parseResponse(responseText) {
|
|||||||
const parsed = repairJSON(jsonContent);
|
const parsed = repairJSON(jsonContent);
|
||||||
|
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
|
const normalizedParsed = unwrapTrackerEnvelope(parsed);
|
||||||
// console.log(`[RPG Parser] Object ${idx + 1} parsed successfully, keys:`, Object.keys(parsed));
|
// console.log(`[RPG Parser] Object ${idx + 1} parsed successfully, keys:`, Object.keys(parsed));
|
||||||
|
|
||||||
// Check if object is wrapped (e.g., {"userStats": {...}})
|
// Check if object is wrapped (e.g., {"userStats": {...}})
|
||||||
// Unwrap single-key objects that match our tracker types
|
// Unwrap single-key objects that match our tracker types
|
||||||
let unwrapped = parsed;
|
let unwrapped = normalizedParsed;
|
||||||
if (Object.keys(parsed).length === 1) {
|
if (Object.keys(parsed).length === 1) {
|
||||||
const key = Object.keys(parsed)[0];
|
const key = Object.keys(parsed)[0];
|
||||||
if (key === 'userStats' || key === 'infoBox' || key === 'characters') {
|
if (key === 'userStats' || key === 'infoBox' || key === 'characters') {
|
||||||
@@ -261,6 +325,22 @@ export function parseResponse(responseText) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for unified structure format (even if previous detection missed it)
|
||||||
|
// This handles the prompt-requested format: {"userStats": {...}, "infoBox": {...}, "characters": [...]}
|
||||||
|
if (normalizedParsed.userStats || normalizedParsed.infoBox || normalizedParsed.characters || normalizedParsed.characterThoughts || normalizedParsed.presentCharacters) {
|
||||||
|
if (normalizedParsed.userStats) {
|
||||||
|
result.userStats = JSON.stringify(normalizedParsed.userStats);
|
||||||
|
}
|
||||||
|
if (normalizedParsed.infoBox) {
|
||||||
|
result.infoBox = JSON.stringify(normalizedParsed.infoBox);
|
||||||
|
}
|
||||||
|
const normalizedCharacters = normalizedParsed.characters || normalizedParsed.presentCharacters || normalizedParsed.characterThoughts;
|
||||||
|
if (normalizedCharacters) {
|
||||||
|
result.characterThoughts = JSON.stringify(normalizedCharacters);
|
||||||
|
}
|
||||||
|
continue; // Skip further classification
|
||||||
|
}
|
||||||
|
|
||||||
// Detect tracker type by checking for top-level fields
|
// Detect tracker type by checking for top-level fields
|
||||||
if (unwrapped.stats || unwrapped.status || unwrapped.skills || unwrapped.inventory || unwrapped.quests) {
|
if (unwrapped.stats || unwrapped.status || unwrapped.skills || unwrapped.inventory || unwrapped.quests) {
|
||||||
result.userStats = jsonContent;
|
result.userStats = jsonContent;
|
||||||
@@ -307,23 +387,38 @@ export function parseResponse(responseText) {
|
|||||||
for (let idx = 0; idx < jsonMatches.length; idx++) {
|
for (let idx = 0; idx < jsonMatches.length; idx++) {
|
||||||
const match = jsonMatches[idx];
|
const match = jsonMatches[idx];
|
||||||
const jsonContent = match[1].trim();
|
const jsonContent = match[1].trim();
|
||||||
|
|
||||||
|
if (!jsonContent) continue;
|
||||||
|
|
||||||
// console.log(`[RPG Parser] Parsing JSON block ${idx + 1}:`, jsonContent.substring(0, 100) + '...');
|
// console.log(`[RPG Parser] Parsing JSON block ${idx + 1}:`, jsonContent.substring(0, 100) + '...');
|
||||||
|
|
||||||
const parsed = repairJSON(jsonContent);
|
const parsed = repairJSON(jsonContent);
|
||||||
|
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
|
const normalizedParsed = unwrapTrackerEnvelope(parsed);
|
||||||
// console.log(`[RPG Parser] JSON block ${idx + 1} parsed successfully, keys:`, Object.keys(parsed));
|
// console.log(`[RPG Parser] JSON block ${idx + 1} parsed successfully, keys:`, Object.keys(parsed));
|
||||||
|
|
||||||
// Detect tracker type by checking for top-level fields
|
// Detect tracker type by checking for top-level fields
|
||||||
if (parsed.stats || parsed.status || parsed.skills || parsed.inventory || parsed.quests) {
|
if (normalizedParsed.userStats || normalizedParsed.infoBox || normalizedParsed.characters || normalizedParsed.characterThoughts || normalizedParsed.presentCharacters) {
|
||||||
|
if (normalizedParsed.userStats) {
|
||||||
|
result.userStats = JSON.stringify(normalizedParsed.userStats);
|
||||||
|
}
|
||||||
|
if (normalizedParsed.infoBox) {
|
||||||
|
result.infoBox = JSON.stringify(normalizedParsed.infoBox);
|
||||||
|
}
|
||||||
|
const normalizedCharacters = normalizedParsed.characters || normalizedParsed.presentCharacters || normalizedParsed.characterThoughts;
|
||||||
|
if (normalizedCharacters) {
|
||||||
|
result.characterThoughts = JSON.stringify(normalizedCharacters);
|
||||||
|
}
|
||||||
|
} else if (normalizedParsed.stats || normalizedParsed.status || normalizedParsed.skills || normalizedParsed.inventory || normalizedParsed.quests) {
|
||||||
result.userStats = jsonContent;
|
result.userStats = jsonContent;
|
||||||
// console.log('[RPG Parser] ✓ Assigned to User Stats');
|
// console.log('[RPG Parser] ✓ Assigned to User Stats');
|
||||||
debugLog('[RPG Parser] ✓ Extracted JSON User Stats');
|
debugLog('[RPG Parser] ✓ Extracted JSON User Stats');
|
||||||
} else if (parsed.date || parsed.location || parsed.weather || parsed.temperature || parsed.time) {
|
} else if (normalizedParsed.date || normalizedParsed.location || normalizedParsed.weather || normalizedParsed.temperature || normalizedParsed.time) {
|
||||||
result.infoBox = jsonContent;
|
result.infoBox = jsonContent;
|
||||||
// console.log('[RPG Parser] ✓ Assigned to Info Box');
|
// console.log('[RPG Parser] ✓ Assigned to Info Box');
|
||||||
debugLog('[RPG Parser] ✓ Extracted JSON Info Box');
|
debugLog('[RPG Parser] ✓ Extracted JSON Info Box');
|
||||||
} else if (parsed.characters || Array.isArray(parsed)) {
|
} else if (normalizedParsed.characters || normalizedParsed.presentCharacters || normalizedParsed.characterThoughts || Array.isArray(normalizedParsed)) {
|
||||||
result.characterThoughts = jsonContent;
|
result.characterThoughts = jsonContent;
|
||||||
// console.log('[RPG Parser] ✓ Assigned to Characters');
|
// console.log('[RPG Parser] ✓ Assigned to Characters');
|
||||||
debugLog('[RPG Parser] ✓ Extracted JSON Characters');
|
debugLog('[RPG Parser] ✓ Extracted JSON Characters');
|
||||||
@@ -363,6 +458,9 @@ export function parseResponse(responseText) {
|
|||||||
debugLog('[RPG Parser] Found JSON blocks within XML tags');
|
debugLog('[RPG Parser] Found JSON blocks within XML tags');
|
||||||
for (const match of xmlJsonMatches) {
|
for (const match of xmlJsonMatches) {
|
||||||
const jsonContent = match[1].trim();
|
const jsonContent = match[1].trim();
|
||||||
|
|
||||||
|
if (!jsonContent) continue;
|
||||||
|
|
||||||
const parsed = repairJSON(jsonContent);
|
const parsed = repairJSON(jsonContent);
|
||||||
|
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
@@ -500,10 +598,47 @@ export function parseResponse(responseText) {
|
|||||||
debugLog('[RPG Parser] Found Characters:', !!result.characterThoughts);
|
debugLog('[RPG Parser] Found Characters:', !!result.characterThoughts);
|
||||||
debugLog('[RPG Parser] =======================================================');
|
debugLog('[RPG Parser] =======================================================');
|
||||||
|
|
||||||
|
// Final fallback: try to extract tracker JSON from any fenced block content
|
||||||
|
// This catches responses where JSON is embedded in non-standard markdown structure.
|
||||||
|
if (!result.userStats && !result.infoBox && !result.characterThoughts) {
|
||||||
|
const fencedRegex = /```(?:json)?\s*\n?([\s\S]*?)```/gi;
|
||||||
|
const fencedMatches = [...cleanedResponse.matchAll(fencedRegex)];
|
||||||
|
|
||||||
|
for (const match of fencedMatches) {
|
||||||
|
const fencedContent = (match[1] || '').trim();
|
||||||
|
if (!fencedContent) continue;
|
||||||
|
|
||||||
|
const extracted = extractJSONFromText(fencedContent) || fencedContent;
|
||||||
|
const parsed = repairJSON(extracted);
|
||||||
|
const normalizedParsed = parsed ? unwrapTrackerEnvelope(parsed) : null;
|
||||||
|
if (!normalizedParsed) continue;
|
||||||
|
|
||||||
|
if (normalizedParsed.userStats && !result.userStats) {
|
||||||
|
result.userStats = JSON.stringify(normalizedParsed.userStats);
|
||||||
|
}
|
||||||
|
if (normalizedParsed.infoBox && !result.infoBox) {
|
||||||
|
result.infoBox = JSON.stringify(normalizedParsed.infoBox);
|
||||||
|
}
|
||||||
|
const normalizedCharacters = normalizedParsed.characters || normalizedParsed.presentCharacters || normalizedParsed.characterThoughts;
|
||||||
|
if (normalizedCharacters && !result.characterThoughts) {
|
||||||
|
result.characterThoughts = JSON.stringify(normalizedCharacters);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.userStats || result.infoBox || result.characterThoughts) {
|
||||||
|
debugLog('[RPG Parser] ✓ Extracted trackers from final fenced-block fallback');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we found at least one section - if not, mark as parsing failure
|
// Check if we found at least one section - if not, mark as parsing failure
|
||||||
if (!result.userStats && !result.infoBox && !result.characterThoughts) {
|
if (!result.userStats && !result.infoBox && !result.characterThoughts) {
|
||||||
result.parsingFailed = true;
|
result.parsingFailed = true;
|
||||||
console.error('[RPG Parser] ❌ No tracker data found in response - parsing failed');
|
if (!suppressNoDataError) {
|
||||||
|
console.error('[RPG Parser] ❌ No tracker data found in response - parsing failed');
|
||||||
|
} else {
|
||||||
|
debugLog('[RPG Parser] No tracker data found (suppressed no-data error)');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -524,7 +659,7 @@ export function parseUserStats(statsText) {
|
|||||||
// Check if this is v3 JSON format - try to parse it first
|
// Check if this is v3 JSON format - try to parse it first
|
||||||
let statsData = null;
|
let statsData = null;
|
||||||
const trimmed = statsText.trim();
|
const trimmed = statsText.trim();
|
||||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
if (trimmed && (trimmed.startsWith('{') || trimmed.startsWith('['))) {
|
||||||
statsData = repairJSON(statsText);
|
statsData = repairJSON(statsText);
|
||||||
if (statsData) {
|
if (statsData) {
|
||||||
debugLog('[RPG Parser] ✓ Parsed as v3 JSON format');
|
debugLog('[RPG Parser] ✓ Parsed as v3 JSON format');
|
||||||
@@ -547,9 +682,17 @@ export function parseUserStats(statsText) {
|
|||||||
extensionSettings.userStats.mood = statsData.status.mood;
|
extensionSettings.userStats.mood = statsData.status.mood;
|
||||||
// console.log('[RPG Parser] ✓ Set mood =', statsData.status.mood);
|
// console.log('[RPG Parser] ✓ Set mood =', statsData.status.mood);
|
||||||
}
|
}
|
||||||
if (statsData.status.conditions) {
|
// Extract all custom status fields
|
||||||
extensionSettings.userStats.conditions = statsData.status.conditions;
|
const trackerConfig = extensionSettings.trackerConfig;
|
||||||
// console.log('[RPG Parser] ✓ Set conditions =', statsData.status.conditions);
|
const customFields = trackerConfig?.userStats?.statusSection?.customFields || [];
|
||||||
|
for (const fieldName of customFields) {
|
||||||
|
const fieldKey = toFieldKey(fieldName);
|
||||||
|
// Try the base key first (e.g., "conditions"), then fall back to full lowercase name
|
||||||
|
const value = statsData.status[fieldKey] || statsData.status[fieldName.toLowerCase()];
|
||||||
|
if (value) {
|
||||||
|
extensionSettings.userStats[fieldKey] = value;
|
||||||
|
// console.log(`[RPG Parser] ✓ Set ${fieldKey} =`, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -603,6 +746,13 @@ export function parseUserStats(statsText) {
|
|||||||
if (!quest) return '';
|
if (!quest) return '';
|
||||||
if (typeof quest === 'string') return quest;
|
if (typeof quest === 'string') return quest;
|
||||||
if (typeof quest === 'object') {
|
if (typeof quest === 'object') {
|
||||||
|
// Check for locked format: {value, locked}
|
||||||
|
// Recursively extract value if it's nested
|
||||||
|
let extracted = quest;
|
||||||
|
while (typeof extracted === 'object' && extracted.value !== undefined) {
|
||||||
|
extracted = extracted.value;
|
||||||
|
}
|
||||||
|
if (typeof extracted === 'string') return extracted;
|
||||||
// v3 format: {title, description, status}
|
// v3 format: {title, description, status}
|
||||||
return quest.title || quest.description || JSON.stringify(quest);
|
return quest.title || quest.description || JSON.stringify(quest);
|
||||||
}
|
}
|
||||||
@@ -679,6 +829,7 @@ export function parseUserStats(statsText) {
|
|||||||
const statusConfig = trackerConfig?.userStats?.statusSection;
|
const statusConfig = trackerConfig?.userStats?.statusSection;
|
||||||
if (statusConfig?.enabled) {
|
if (statusConfig?.enabled) {
|
||||||
let moodMatch = null;
|
let moodMatch = null;
|
||||||
|
const customFields = statusConfig.customFields || [];
|
||||||
|
|
||||||
// Try Status: format
|
// Try Status: format
|
||||||
const statusMatch = statsText.match(/Status:\s*(.+)/i);
|
const statusMatch = statsText.match(/Status:\s*(.+)/i);
|
||||||
@@ -691,14 +842,30 @@ export function parseUserStats(statsText) {
|
|||||||
if (emoji) {
|
if (emoji) {
|
||||||
extensionSettings.userStats.mood = emoji;
|
extensionSettings.userStats.mood = emoji;
|
||||||
// Remaining text contains custom status fields
|
// Remaining text contains custom status fields
|
||||||
if (text) {
|
if (text && customFields.length > 0) {
|
||||||
extensionSettings.userStats.conditions = text;
|
// For first custom field, use the remaining text
|
||||||
|
const firstFieldKey = customFields[0].toLowerCase();
|
||||||
|
extensionSettings.userStats[firstFieldKey] = text;
|
||||||
}
|
}
|
||||||
moodMatch = true;
|
moodMatch = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No mood emoji, whole status is conditions
|
// No mood emoji, whole status goes to first custom field
|
||||||
extensionSettings.userStats.conditions = statusContent;
|
if (customFields.length > 0) {
|
||||||
|
const firstFieldKey = customFields[0].toLowerCase();
|
||||||
|
extensionSettings.userStats[firstFieldKey] = statusContent;
|
||||||
|
}
|
||||||
|
moodMatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract individual custom status fields by name
|
||||||
|
for (const fieldName of customFields) {
|
||||||
|
const fieldKey = fieldName.toLowerCase();
|
||||||
|
const fieldRegex = new RegExp(`${fieldName}:\\s*(.+?)(?:,|$)`, 'i');
|
||||||
|
const fieldMatch = statsText.match(fieldRegex);
|
||||||
|
if (fieldMatch) {
|
||||||
|
extensionSettings.userStats[fieldKey] = fieldMatch[1].trim();
|
||||||
moodMatch = true;
|
moodMatch = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,7 +873,10 @@ export function parseUserStats(statsText) {
|
|||||||
debugLog('[RPG Parser] Status match:', {
|
debugLog('[RPG Parser] Status match:', {
|
||||||
found: !!moodMatch,
|
found: !!moodMatch,
|
||||||
mood: extensionSettings.userStats.mood,
|
mood: extensionSettings.userStats.mood,
|
||||||
conditions: extensionSettings.userStats.conditions
|
customFields: customFields.map(f => ({
|
||||||
|
name: f,
|
||||||
|
value: extensionSettings.userStats[f.toLowerCase()]
|
||||||
|
}))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
addLockInstruction
|
addLockInstruction
|
||||||
} from './jsonPromptHelpers.js';
|
} from './jsonPromptHelpers.js';
|
||||||
import { applyLocks } from './lockManager.js';
|
import { applyLocks } from './lockManager.js';
|
||||||
|
import { isPresentCharactersEnabled } from '../../utils/presentCharacters.js';
|
||||||
|
|
||||||
// Type imports
|
// Type imports
|
||||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||||
@@ -28,6 +29,25 @@ export const DEFAULT_HTML_PROMPT = `If appropriate, include inline HTML, CSS, an
|
|||||||
*/
|
*/
|
||||||
export const DEFAULT_DIALOGUE_COLORING_PROMPT = `Wrap all character/NPC "dialogues" in unique <font color=######>tags</font>, exemplary: <font color=#abc123>"You're pretty good."</font> Assign a distinct color to each speaker and reuse it whenever they speak again.`;
|
export const DEFAULT_DIALOGUE_COLORING_PROMPT = `Wrap all character/NPC "dialogues" in unique <font color=######>tags</font>, exemplary: <font color=#abc123>"You're pretty good."</font> Assign a distinct color to each speaker and reuse it whenever they speak again.`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Deception System prompt text
|
||||||
|
*/
|
||||||
|
export const DEFAULT_DECEPTION_PROMPT = `When a character is lying or deceiving, you should follow up that line with the <lie> tag, containing a brief description of the truth and the lie's reason, using the template below (replace placeholders in quotation marks). This will be hidden from the user's view, but not to you, making it useful for future consequences: <lie character="name" type="lying/deceiving/omitting" truth="truth" reason="reason"/>.`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Omniscience Filter prompt text
|
||||||
|
* This instructs the AI to separate information the player character cannot perceive
|
||||||
|
*/
|
||||||
|
export const DEFAULT_OMNISCIENCE_FILTER_PROMPT = `You must strictly separate what the player can directly perceive from what they cannot. They should only read limited narrative content that their persona can actually see, hear, smell, touch, or otherwise directly sense. Before writing any narrative content that involves events, actions, or details the player directly cannot perceive (because they're not looking, too far away, behind them, in another room, happening silently, include NPCs' internal thoughts, etc.), you absolutely must output that hidden information inside a <ofilter> tag using this exact format:
|
||||||
|
<ofilter event="[Brief description of what is happening that the player cannot perceive]" reason="[Why the player character cannot perceive this - e.g., 'behind them', 'in another room', 'too quiet to hear', 'focused elsewhere']"/>
|
||||||
|
Example: <ofilter event="Zandik quietly takes the key from the table and slips out the back door" reason="Zandik is behind Mari, who is absorbed in reading, and he moves silently"/> You hear a faint click from somewhere behind you, but when you glance up from your newspaper, the room seems unchanged.`;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default CYOA prompt text
|
||||||
|
*/
|
||||||
|
export const DEFAULT_CYOA_PROMPT = `Since this is a "Choose Your Own Adventure" type of game, you must finish your response by creating a numbered list of 5 different possible action or dialogue options (depending on the scene) for the user to choose from. Make sure they all fit their persona well. They will respond with their choice on how to progress.`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default Spotify music prompt text (customizable by users)
|
* Default Spotify music prompt text (customizable by users)
|
||||||
*/
|
*/
|
||||||
@@ -43,6 +63,11 @@ export const SPOTIFY_FORMAT_INSTRUCTION = `Include it in this exact format: <spo
|
|||||||
*/
|
*/
|
||||||
export const DEFAULT_NARRATOR_PROMPT = `Infer the identity and details of characters present in each scene from the story context below. Do not use fixed character references; instead, identify characters naturally based on their actions, dialogue, and descriptions in the narrative.`;
|
export const DEFAULT_NARRATOR_PROMPT = `Infer the identity and details of characters present in each scene from the story context below. Do not use fixed character references; instead, identify characters naturally based on their actions, dialogue, and descriptions in the narrative.`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Context Instructions prompt text (customizable by users)
|
||||||
|
*/
|
||||||
|
export const DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT = `The context above is information about the current scene, and what follows is the last message in the chat history. Ensure these details naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting performance, low hygiene influencing social interactions, environmental factors shaping the scene, or a character's emotional state coloring their responses.`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets character card information for current chat (handles both single and group chats)
|
* Gets character card information for current chat (handles both single and group chats)
|
||||||
* @returns {string} Formatted character information
|
* @returns {string} Formatted character information
|
||||||
@@ -229,7 +254,6 @@ function buildAttributesString() {
|
|||||||
*/
|
*/
|
||||||
export function generateTrackerExample() {
|
export function generateTrackerExample() {
|
||||||
let example = '';
|
let example = '';
|
||||||
const useXmlTags = extensionSettings.saveTrackerHistory;
|
|
||||||
|
|
||||||
// Use COMMITTED data for generation context, not displayed data
|
// Use COMMITTED data for generation context, not displayed data
|
||||||
// Apply locks before sending to AI (for JSON format only)
|
// Apply locks before sending to AI (for JSON format only)
|
||||||
@@ -270,7 +294,7 @@ export function generateTrackerExample() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
|
if (isPresentCharactersEnabled() && committedTrackerData.characterThoughts) {
|
||||||
try {
|
try {
|
||||||
JSON.parse(committedTrackerData.characterThoughts);
|
JSON.parse(committedTrackerData.characterThoughts);
|
||||||
const lockedData = applyLocks(committedTrackerData.characterThoughts, 'characters');
|
const lockedData = applyLocks(committedTrackerData.characterThoughts, 'characters');
|
||||||
@@ -306,30 +330,22 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
let instructions = '';
|
let instructions = '';
|
||||||
|
|
||||||
// Check if any trackers are enabled
|
// Check if any trackers are enabled
|
||||||
const hasAnyTrackers = extensionSettings.showUserStats || extensionSettings.showInfoBox || extensionSettings.showCharacterThoughts;
|
const hasAnyTrackers = extensionSettings.showUserStats || extensionSettings.showInfoBox || isPresentCharactersEnabled();
|
||||||
|
|
||||||
// Only add tracker instructions if at least one tracker is enabled
|
// Only add tracker instructions if at least one tracker is enabled
|
||||||
if (hasAnyTrackers) {
|
if (hasAnyTrackers) {
|
||||||
// Determine format based on saveTrackerHistory setting
|
|
||||||
const useXmlTags = extensionSettings.saveTrackerHistory;
|
|
||||||
const openTag = useXmlTags ? '<trackers>\n' : '';
|
|
||||||
const closeTag = useXmlTags ? '\n</trackers>' : '';
|
|
||||||
const codeBlockMarker = '';
|
const codeBlockMarker = '';
|
||||||
const endCodeBlockMarker = '';
|
const endCodeBlockMarker = '';
|
||||||
|
|
||||||
// Universal instruction header
|
// Universal instruction header
|
||||||
if (useXmlTags) {
|
instructions += '\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the JSON format shown below as a single unified JSON object containing all enabled tracker fields. ';
|
||||||
instructions += `\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the JSON format shown below, enclosed in <trackers></trackers> XML tags. `;
|
|
||||||
} else {
|
|
||||||
instructions += '\nAt the start of every reply, you must attach an update to the trackers in EXACTLY the JSON format shown below as a single unified JSON object containing all enabled tracker fields. ';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append custom instruction portion if available
|
// Append custom instruction portion if available
|
||||||
const customPrompt = extensionSettings.customTrackerInstructionsPrompt;
|
const customPrompt = extensionSettings.customTrackerInstructionsPrompt;
|
||||||
if (customPrompt) {
|
if (customPrompt) {
|
||||||
instructions += customPrompt.replace(/{userName}/g, userName);
|
instructions += customPrompt.replace(/{userName}/g, userName);
|
||||||
} else {
|
} else {
|
||||||
instructions += `Replace X with actual numbers (e.g., 69) and replace all placeholders with concrete in-world details that ${userName} perceives about the current scene and the present characters. For example: "Location" becomes "Forest Clearing", "Mood Emoji" becomes "😊". `;
|
instructions += `Replace X with actual numbers (e.g., 69) and replace all placeholders with concrete in-world details that ${userName} perceives about the current scene and the present characters. For example: "Location" becomes "Forest Clearing", "Mood Emoji" becomes "😊". DO NOT include ${userName} in the characters section, only NPCs. `;
|
||||||
instructions += `Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences.`;
|
instructions += `Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user's actions, the passage of time, and logical consequences.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +361,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
if (extensionSettings.showInfoBox) {
|
if (extensionSettings.showInfoBox) {
|
||||||
enabledTrackers.push('infoBox');
|
enabledTrackers.push('infoBox');
|
||||||
}
|
}
|
||||||
if (extensionSettings.showCharacterThoughts) {
|
if (isPresentCharactersEnabled()) {
|
||||||
enabledTrackers.push('characters');
|
enabledTrackers.push('characters');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,7 +384,7 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
instructions += enabledTrackers.indexOf('infoBox') < enabledTrackers.length - 1 ? ',\n' : '\n';
|
instructions += enabledTrackers.indexOf('infoBox') < enabledTrackers.length - 1 ? ',\n' : '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extensionSettings.showCharacterThoughts) {
|
if (isPresentCharactersEnabled()) {
|
||||||
instructions += ' "characters": ';
|
instructions += ' "characters": ';
|
||||||
const charactersJSON = buildCharactersJSONInstruction();
|
const charactersJSON = buildCharactersJSONInstruction();
|
||||||
// Add 2 spaces to all lines after the first to properly nest within root object
|
// Add 2 spaces to all lines after the first to properly nest within root object
|
||||||
@@ -391,21 +407,30 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon
|
|||||||
// Include attributes based on settings (only if includeAttributes is true)
|
// Include attributes based on settings (only if includeAttributes is true)
|
||||||
if (includeAttributes) {
|
if (includeAttributes) {
|
||||||
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
||||||
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
|
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes !== false;
|
||||||
|
const shouldSendAttributes = alwaysSendAttributes && showRPGAttributes;
|
||||||
|
|
||||||
if (shouldSendAttributes) {
|
if (shouldSendAttributes) {
|
||||||
const attributesString = buildAttributesString();
|
const attributesString = buildAttributesString();
|
||||||
instructions += `${userName}'s attributes: ${attributesString}\n`;
|
instructions += `${userName}'s attributes: ${attributesString}\n`;
|
||||||
|
|
||||||
// Add dice roll context if there was one
|
|
||||||
if (extensionSettings.lastDiceRoll) {
|
|
||||||
const roll = extensionSettings.lastDiceRoll;
|
|
||||||
instructions += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
|
|
||||||
} else {
|
|
||||||
instructions += `\n`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add dice roll context if there was one (independent of attributes)
|
||||||
|
if (extensionSettings.lastDiceRoll) {
|
||||||
|
const roll = extensionSettings.lastDiceRoll;
|
||||||
|
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes !== false;
|
||||||
|
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
||||||
|
const hasAttributes = includeAttributes && (alwaysSendAttributes && showRPGAttributes);
|
||||||
|
|
||||||
|
if (hasAttributes) {
|
||||||
|
instructions += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
|
||||||
|
} else {
|
||||||
|
instructions += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Decide whether they succeeded or failed the action they attempted.\n\n`;
|
||||||
|
}
|
||||||
|
} else if (includeAttributes && trackerConfig?.userStats?.alwaysSendAttributes && trackerConfig?.userStats?.showRPGAttributes !== false) {
|
||||||
|
instructions += `\n`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append HTML prompt if enabled AND includeHtmlPrompt is true
|
// Append HTML prompt if enabled AND includeHtmlPrompt is true
|
||||||
@@ -475,11 +500,22 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
|
|||||||
|
|
||||||
// Handle common object formats
|
// Handle common object formats
|
||||||
if (field && typeof field === 'object') {
|
if (field && typeof field === 'object') {
|
||||||
// Status object: {mood, conditions}
|
// Status object: {mood, [customFields...]}
|
||||||
if ('mood' in field && 'conditions' in field) {
|
if ('mood' in field) {
|
||||||
|
const statusParts = [];
|
||||||
const mood = getValue(field.mood);
|
const mood = getValue(field.mood);
|
||||||
const conditions = getValue(field.conditions);
|
if (mood) statusParts.push(mood);
|
||||||
return `${mood} - ${conditions}`;
|
|
||||||
|
// Add all other status fields (custom fields)
|
||||||
|
for (const [key, value] of Object.entries(field)) {
|
||||||
|
if (key !== 'mood') {
|
||||||
|
const fieldValue = getValue(value);
|
||||||
|
if (fieldValue && fieldValue !== 'None') {
|
||||||
|
statusParts.push(fieldValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return statusParts.join(' - ');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skill/item/quest objects: {name}, {title}, {name, quantity}
|
// Skill/item/quest objects: {name}, {title}, {name, quantity}
|
||||||
@@ -525,12 +561,33 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
|
|||||||
if (trackerType === 'userStats') {
|
if (trackerType === 'userStats') {
|
||||||
formatted += `${userName}'s Stats:\n`;
|
formatted += `${userName}'s Stats:\n`;
|
||||||
|
|
||||||
|
// Get display mode and custom stats config for maxValue lookup
|
||||||
|
const userStatsConfig = extensionSettings.trackerConfig?.userStats;
|
||||||
|
const displayMode = userStatsConfig?.statsDisplayMode || 'percentage';
|
||||||
|
const customStats = userStatsConfig?.customStats || [];
|
||||||
|
|
||||||
|
// Helper to get maxValue for a stat by id
|
||||||
|
const getMaxValue = (statId) => {
|
||||||
|
const statConfig = customStats.find(s => s.id === statId);
|
||||||
|
return statConfig?.maxValue || 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to format stat value based on display mode
|
||||||
|
const formatStatValue = (value, statId) => {
|
||||||
|
if (displayMode === 'number') {
|
||||||
|
const maxValue = getMaxValue(statId);
|
||||||
|
return `${value}/${maxValue}`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
// Handle stats array format: [{id, name, value}, ...]
|
// Handle stats array format: [{id, name, value}, ...]
|
||||||
if (data.stats && Array.isArray(data.stats)) {
|
if (data.stats && Array.isArray(data.stats)) {
|
||||||
for (const stat of data.stats) {
|
for (const stat of data.stats) {
|
||||||
if (stat && stat.value !== undefined) {
|
if (stat && stat.value !== undefined) {
|
||||||
const statName = stat.name || (stat.id ? stat.id.charAt(0).toUpperCase() + stat.id.slice(1) : 'Unknown');
|
const statName = stat.name || (stat.id ? stat.id.charAt(0).toUpperCase() + stat.id.slice(1) : 'Unknown');
|
||||||
formatted += `${statName}: ${stat.value}\n`;
|
const statId = stat.id || statName.toLowerCase();
|
||||||
|
formatted += `${statName}: ${formatStatValue(stat.value, statId)}\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -543,7 +600,7 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
|
|||||||
const value = getValue(data[statName]);
|
const value = getValue(data[statName]);
|
||||||
if (value) {
|
if (value) {
|
||||||
const displayName = statName.charAt(0).toUpperCase() + statName.slice(1);
|
const displayName = statName.charAt(0).toUpperCase() + statName.slice(1);
|
||||||
formatted += `${displayName}: ${value}\n`;
|
formatted += `${displayName}: ${formatStatValue(value, statName)}\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -552,7 +609,7 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
|
|||||||
for (const [key, value] of Object.entries(data)) {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
if (!statFieldOrder.includes(key) && !specialFields.includes(key) && typeof value === 'number') {
|
if (!statFieldOrder.includes(key) && !specialFields.includes(key) && typeof value === 'number') {
|
||||||
const displayName = key.charAt(0).toUpperCase() + key.slice(1);
|
const displayName = key.charAt(0).toUpperCase() + key.slice(1);
|
||||||
formatted += `${displayName}: ${getValue(value)}\n`;
|
formatted += `${displayName}: ${formatStatValue(getValue(value), key)}\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -683,13 +740,14 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relationship
|
// Relationship - check both Relationship (new format) and relationship (old format)
|
||||||
if (char.relationship) {
|
const relationshipValue = char.Relationship || char.relationship;
|
||||||
|
if (relationshipValue) {
|
||||||
let relValue;
|
let relValue;
|
||||||
if (typeof char.relationship === 'object' && !Array.isArray(char.relationship) && 'status' in char.relationship) {
|
if (typeof relationshipValue === 'object' && !Array.isArray(relationshipValue) && 'status' in relationshipValue) {
|
||||||
relValue = getValue(char.relationship.status);
|
relValue = getValue(relationshipValue.status);
|
||||||
} else {
|
} else {
|
||||||
relValue = getValue(char.relationship);
|
relValue = getValue(relationshipValue);
|
||||||
}
|
}
|
||||||
if (relValue) formatted += ` Relationship: ${relValue}\n`;
|
if (relValue) formatted += ` Relationship: ${relValue}\n`;
|
||||||
}
|
}
|
||||||
@@ -728,6 +786,244 @@ function formatTrackerDataForContext(jsonData, trackerType, userName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats historical tracker data from a message's rpg_companion_swipes data.
|
||||||
|
* Only includes tracker fields that have persistInHistory enabled in trackerConfig,
|
||||||
|
* unless useAllEnabled is true, in which case it includes all enabled fields.
|
||||||
|
* Uses the same formatting as formatTrackerDataForContext but filtered by persistence settings.
|
||||||
|
*
|
||||||
|
* @param {Object} trackerData - The tracker data from message.extra.rpg_companion_swipes[swipeId]
|
||||||
|
* @param {Object} trackerConfig - The tracker configuration from extensionSettings.trackerConfig
|
||||||
|
* @param {string} userName - The user's name for personalization
|
||||||
|
* @param {boolean} [useAllEnabled=false] - If true, include all enabled fields instead of only persistInHistory fields
|
||||||
|
* @returns {string} Formatted historical context or empty string if nothing to include
|
||||||
|
*/
|
||||||
|
export function formatHistoricalTrackerData(trackerData, trackerConfig, userName, useAllEnabled = false) {
|
||||||
|
if (!trackerData || !trackerConfig) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to check if a field should be included
|
||||||
|
const shouldInclude = (config) => {
|
||||||
|
if (useAllEnabled) {
|
||||||
|
return config?.enabled !== false; // Include if enabled (default true for most fields)
|
||||||
|
}
|
||||||
|
return config?.persistInHistory === true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to check if a stat/attribute should be included
|
||||||
|
const shouldIncludeStat = (configStat) => {
|
||||||
|
if (useAllEnabled) {
|
||||||
|
return configStat?.enabled !== false;
|
||||||
|
}
|
||||||
|
return configStat?.persistInHistory === true;
|
||||||
|
};
|
||||||
|
|
||||||
|
let formatted = '';
|
||||||
|
|
||||||
|
// Helper to safely get values
|
||||||
|
const getValue = (field) => {
|
||||||
|
if (field === null || field === undefined) return '';
|
||||||
|
if (field && typeof field === 'object' && !Array.isArray(field) && 'value' in field) {
|
||||||
|
return getValue(field.value);
|
||||||
|
}
|
||||||
|
if (typeof field !== 'object') {
|
||||||
|
return String(field);
|
||||||
|
}
|
||||||
|
if (Array.isArray(field)) {
|
||||||
|
return field.map(item => getValue(item)).filter(Boolean).join(', ');
|
||||||
|
}
|
||||||
|
if (field && typeof field === 'object') {
|
||||||
|
if ('start' in field && 'end' in field) {
|
||||||
|
return `${getValue(field.start)} - ${getValue(field.end)}`;
|
||||||
|
}
|
||||||
|
if ('emoji' in field && 'forecast' in field) {
|
||||||
|
return `${getValue(field.emoji)} ${getValue(field.forecast)}`;
|
||||||
|
}
|
||||||
|
if ('name' in field) {
|
||||||
|
const name = getValue(field.name);
|
||||||
|
if ('quantity' in field && field.quantity > 1) {
|
||||||
|
return `${name} (x${field.quantity})`;
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
if ('title' in field) {
|
||||||
|
return getValue(field.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Process userStats if present and has persistence-enabled fields
|
||||||
|
if (trackerData.userStats) {
|
||||||
|
const userStatsConfig = trackerConfig.userStats;
|
||||||
|
const userStatsData = typeof trackerData.userStats === 'string'
|
||||||
|
? JSON.parse(trackerData.userStats)
|
||||||
|
: trackerData.userStats;
|
||||||
|
|
||||||
|
let statsFormatted = '';
|
||||||
|
|
||||||
|
// Custom stats with persistInHistory enabled (or enabled if useAllEnabled)
|
||||||
|
if (userStatsData.stats && Array.isArray(userStatsData.stats) && userStatsConfig.customStats) {
|
||||||
|
for (const stat of userStatsData.stats) {
|
||||||
|
const configStat = userStatsConfig.customStats.find(s => s.id === stat.id);
|
||||||
|
if (shouldIncludeStat(configStat) && stat.value !== undefined) {
|
||||||
|
const statName = stat.name || configStat.name || stat.id;
|
||||||
|
statsFormatted += `${statName}: ${stat.value}, `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status section
|
||||||
|
if (shouldInclude(userStatsConfig.statusSection) && userStatsData.status) {
|
||||||
|
const mood = getValue(userStatsData.status.mood || userStatsData.status);
|
||||||
|
if (mood && userStatsConfig.statusSection.showMoodEmoji) statsFormatted += `Mood: ${mood}, `;
|
||||||
|
|
||||||
|
// Add all custom status fields
|
||||||
|
const customFields = userStatsConfig.statusSection.customFields || [];
|
||||||
|
for (const fieldName of customFields) {
|
||||||
|
const fieldKey = fieldName.toLowerCase();
|
||||||
|
const fieldValue = getValue(userStatsData.status[fieldKey]);
|
||||||
|
if (fieldValue && fieldValue !== 'None') {
|
||||||
|
statsFormatted += `${fieldName}: ${fieldValue}, `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skills section
|
||||||
|
if (shouldInclude(userStatsConfig.skillsSection) && userStatsData.skills) {
|
||||||
|
const skillsList = Array.isArray(userStatsData.skills)
|
||||||
|
? userStatsData.skills.map(s => getValue(s)).filter(s => s).join(', ')
|
||||||
|
: getValue(userStatsData.skills);
|
||||||
|
if (skillsList) statsFormatted += `Skills: ${skillsList}, `;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inventory
|
||||||
|
const shouldIncludeInventory = useAllEnabled || userStatsConfig.inventoryPersistInHistory;
|
||||||
|
if (shouldIncludeInventory && userStatsData.inventory) {
|
||||||
|
const inv = userStatsData.inventory;
|
||||||
|
if (inv.onPerson && Array.isArray(inv.onPerson) && inv.onPerson.length > 0) {
|
||||||
|
const items = inv.onPerson.map(i => getValue(i)).filter(i => i);
|
||||||
|
if (items.length > 0) statsFormatted += `On Person: ${items.join(', ')}, `;
|
||||||
|
}
|
||||||
|
if (inv.clothing && Array.isArray(inv.clothing) && inv.clothing.length > 0) {
|
||||||
|
const items = inv.clothing.map(i => getValue(i)).filter(i => i);
|
||||||
|
if (items.length > 0) statsFormatted += `Clothing: ${items.join(', ')}, `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quests
|
||||||
|
const shouldIncludeQuests = useAllEnabled || userStatsConfig.questsPersistInHistory;
|
||||||
|
if (shouldIncludeQuests && userStatsData.quests) {
|
||||||
|
const quests = userStatsData.quests;
|
||||||
|
if (quests.main) {
|
||||||
|
const mainQuest = getValue(quests.main);
|
||||||
|
if (mainQuest && mainQuest !== 'None') statsFormatted += `Quest: ${mainQuest}, `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statsFormatted) {
|
||||||
|
formatted += `${userName}: ${statsFormatted.slice(0, -2)}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process infoBox if present and has persistence-enabled widgets
|
||||||
|
if (trackerData.infoBox) {
|
||||||
|
const infoBoxConfig = trackerConfig.infoBox;
|
||||||
|
const infoBoxData = typeof trackerData.infoBox === 'string'
|
||||||
|
? JSON.parse(trackerData.infoBox)
|
||||||
|
: trackerData.infoBox;
|
||||||
|
|
||||||
|
let infoFormatted = '';
|
||||||
|
|
||||||
|
// Date
|
||||||
|
if (shouldInclude(infoBoxConfig.widgets.date) && infoBoxData.date) {
|
||||||
|
const date = getValue(infoBoxData.date);
|
||||||
|
if (date) infoFormatted += `Date: ${date}, `;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time
|
||||||
|
if (shouldInclude(infoBoxConfig.widgets.time) && infoBoxData.time) {
|
||||||
|
const time = getValue(infoBoxData.time);
|
||||||
|
if (time) infoFormatted += `Time: ${time}, `;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather
|
||||||
|
if (shouldInclude(infoBoxConfig.widgets.weather) && infoBoxData.weather) {
|
||||||
|
const weather = getValue(infoBoxData.weather);
|
||||||
|
if (weather) infoFormatted += `Weather: ${weather}, `;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temperature
|
||||||
|
if (shouldInclude(infoBoxConfig.widgets.temperature) && infoBoxData.temperature) {
|
||||||
|
const temp = getValue(infoBoxData.temperature);
|
||||||
|
if (temp) infoFormatted += `Temp: ${temp}, `;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location
|
||||||
|
if (shouldInclude(infoBoxConfig.widgets.location) && infoBoxData.location) {
|
||||||
|
const location = getValue(infoBoxData.location);
|
||||||
|
if (location) infoFormatted += `Location: ${location}, `;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recent Events
|
||||||
|
if (shouldInclude(infoBoxConfig.widgets.recentEvents) && infoBoxData.recentEvents) {
|
||||||
|
const events = getValue(infoBoxData.recentEvents);
|
||||||
|
if (events) infoFormatted += `Events: ${events}, `;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (infoFormatted) {
|
||||||
|
formatted += infoFormatted.slice(0, -2) + '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process characterThoughts if present and has persistence-enabled fields
|
||||||
|
if (trackerData.characterThoughts) {
|
||||||
|
const charsConfig = trackerConfig.presentCharacters;
|
||||||
|
const charsData = typeof trackerData.characterThoughts === 'string'
|
||||||
|
? JSON.parse(trackerData.characterThoughts)
|
||||||
|
: trackerData.characterThoughts;
|
||||||
|
|
||||||
|
// Characters can be an array or wrapped in an object
|
||||||
|
const characters = Array.isArray(charsData) ? charsData : (charsData.characters || []);
|
||||||
|
|
||||||
|
for (const char of characters) {
|
||||||
|
if (!char || !char.name) continue;
|
||||||
|
|
||||||
|
let charFormatted = '';
|
||||||
|
|
||||||
|
// Custom fields (appearance, demeanor, etc.)
|
||||||
|
if (char.details && typeof char.details === 'object') {
|
||||||
|
for (const field of charsConfig.customFields) {
|
||||||
|
if (shouldIncludeStat(field) && char.details[field.id]) {
|
||||||
|
const value = getValue(char.details[field.id]);
|
||||||
|
if (value) charFormatted += `${field.name}: ${value}, `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thoughts
|
||||||
|
if (shouldInclude(charsConfig.thoughts) && char.thoughts) {
|
||||||
|
const thoughts = typeof char.thoughts === 'object' && char.thoughts.content
|
||||||
|
? getValue(char.thoughts.content)
|
||||||
|
: getValue(char.thoughts);
|
||||||
|
if (thoughts) charFormatted += `Thinking: ${thoughts}, `;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (charFormatted) {
|
||||||
|
formatted += `${getValue(char.name)}: ${charFormatted.slice(0, -2)}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatted.trim();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG Companion] Failed to format historical tracker data:', e);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a formatted contextual summary for SEPARATE mode injection.
|
* Generates a formatted contextual summary for SEPARATE mode injection.
|
||||||
* Includes the full tracker data in original format (without code fences and separators).
|
* Includes the full tracker data in original format (without code fences and separators).
|
||||||
@@ -766,7 +1062,7 @@ export function generateContextualSummary() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add Present Characters tracker data if enabled
|
// Add Present Characters tracker data if enabled
|
||||||
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
|
if (isPresentCharactersEnabled() && committedTrackerData.characterThoughts) {
|
||||||
try {
|
try {
|
||||||
const formatted = formatTrackerDataForContext(committedTrackerData.characterThoughts, 'characters', userName);
|
const formatted = formatTrackerDataForContext(committedTrackerData.characterThoughts, 'characters', userName);
|
||||||
if (formatted) {
|
if (formatted) {
|
||||||
@@ -779,19 +1075,25 @@ export function generateContextualSummary() {
|
|||||||
|
|
||||||
// Include attributes based on settings
|
// Include attributes based on settings
|
||||||
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
const alwaysSendAttributes = trackerConfig?.userStats?.alwaysSendAttributes;
|
||||||
const shouldSendAttributes = alwaysSendAttributes || extensionSettings.lastDiceRoll;
|
const showRPGAttributes = trackerConfig?.userStats?.showRPGAttributes !== false;
|
||||||
|
const shouldSendAttributes = alwaysSendAttributes && showRPGAttributes;
|
||||||
|
|
||||||
if (shouldSendAttributes) {
|
if (shouldSendAttributes) {
|
||||||
const attributesString = buildAttributesString();
|
const attributesString = buildAttributesString();
|
||||||
summary += `${userName}'s attributes: ${attributesString}\n`;
|
summary += `${userName}'s attributes: ${attributesString}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
// Add dice roll context if there was one
|
// Add dice roll context if there was one (independent of attributes)
|
||||||
if (extensionSettings.lastDiceRoll) {
|
if (extensionSettings.lastDiceRoll) {
|
||||||
const roll = extensionSettings.lastDiceRoll;
|
const roll = extensionSettings.lastDiceRoll;
|
||||||
|
|
||||||
|
if (shouldSendAttributes) {
|
||||||
summary += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
|
summary += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Based on their attributes, decide whether they succeeded or failed the action they attempted.\n\n`;
|
||||||
} else {
|
} else {
|
||||||
summary += `\n`;
|
summary += `${userName} rolled ${roll.total} on the last ${roll.formula} roll. Decide whether they succeeded or failed the action they attempted.\n\n`;
|
||||||
}
|
}
|
||||||
|
} else if (shouldSendAttributes) {
|
||||||
|
summary += `\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return summary.trim();
|
return summary.trim();
|
||||||
@@ -844,16 +1146,34 @@ export function generateRPGPromptText() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (extensionSettings.showCharacterThoughts && committedTrackerData.characterThoughts) {
|
// Include Present Characters data if it exists, regardless of current showCharacterThoughts setting
|
||||||
|
// This ensures existing character data is preserved in context even if the setting is toggled off
|
||||||
|
if (committedTrackerData.characterThoughts) {
|
||||||
try {
|
try {
|
||||||
// Try to parse as JSON - apply locks before adding to previous
|
let parsed;
|
||||||
const lockedData = applyLocks(committedTrackerData.characterThoughts, 'characters');
|
// Check if it's already a JavaScript object/array (not a JSON string)
|
||||||
const parsed = JSON.parse(lockedData);
|
if (typeof committedTrackerData.characterThoughts === 'object') {
|
||||||
unifiedPrevious.characters = parsed;
|
// Already parsed - apply locks and use directly
|
||||||
} catch {
|
parsed = applyLocks(committedTrackerData.characterThoughts, 'characters');
|
||||||
|
} else {
|
||||||
|
// It's a JSON string - apply locks and parse
|
||||||
|
const lockedData = applyLocks(committedTrackerData.characterThoughts, 'characters');
|
||||||
|
parsed = JSON.parse(lockedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include if there's actual character data (non-empty array or object with content)
|
||||||
|
if (parsed && ((Array.isArray(parsed) && parsed.length > 0) ||
|
||||||
|
(parsed.characters && Array.isArray(parsed.characters) && parsed.characters.length > 0))) {
|
||||||
|
unifiedPrevious.characters = parsed;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// console.warn('[RPG Companion] Failed to process characters for previous section:', e);
|
||||||
// Old text format - show it separately for backward compat
|
// Old text format - show it separately for backward compat
|
||||||
if (!unifiedPrevious.userStats && !unifiedPrevious.infoBox) {
|
if (!unifiedPrevious.userStats && !unifiedPrevious.infoBox) {
|
||||||
promptText += `${committedTrackerData.characterThoughts}\n`;
|
const charText = typeof committedTrackerData.characterThoughts === 'string'
|
||||||
|
? committedTrackerData.characterThoughts
|
||||||
|
: JSON.stringify(committedTrackerData.characterThoughts, null, 2);
|
||||||
|
promptText += `${charText}\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -883,6 +1203,8 @@ export function generateRPGPromptText() {
|
|||||||
export async function generateSeparateUpdatePrompt() {
|
export async function generateSeparateUpdatePrompt() {
|
||||||
const depth = extensionSettings.updateDepth;
|
const depth = extensionSettings.updateDepth;
|
||||||
const userName = getContext().name1;
|
const userName = getContext().name1;
|
||||||
|
const trackerConfig = extensionSettings.trackerConfig;
|
||||||
|
const historyPersistence = extensionSettings.historyPersistence;
|
||||||
|
|
||||||
const messages = [];
|
const messages = [];
|
||||||
|
|
||||||
@@ -899,6 +1221,7 @@ export async function generateSeparateUpdatePrompt() {
|
|||||||
systemMessage += `Here is the description of the protagonist for reference:\n`;
|
systemMessage += `Here is the description of the protagonist for reference:\n`;
|
||||||
systemMessage += `<protagonist>\n{{persona}}\n</protagonist>\n`;
|
systemMessage += `<protagonist>\n{{persona}}\n</protagonist>\n`;
|
||||||
systemMessage += `\n`;
|
systemMessage += `\n`;
|
||||||
|
|
||||||
systemMessage += `Here are the last few messages in the conversation history (between the user and the roleplayer assistant) you should reference when responding:\n<history>`;
|
systemMessage += `Here are the last few messages in the conversation history (between the user and the roleplayer assistant) you should reference when responding:\n<history>`;
|
||||||
|
|
||||||
messages.push({
|
messages.push({
|
||||||
@@ -907,13 +1230,112 @@ export async function generateSeparateUpdatePrompt() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// /hide command automatically handles checkpoint filtering
|
// /hide command automatically handles checkpoint filtering
|
||||||
// Add chat history as separate user/assistant messages
|
// Add chat history as separate user/assistant messages with per-message historical context
|
||||||
const recentMessages = chat.slice(-depth);
|
const recentMessages = chat.slice(-depth);
|
||||||
|
const startIndex = chat.length - depth;
|
||||||
|
const position = historyPersistence?.injectionPosition || 'assistant_message_end';
|
||||||
|
|
||||||
|
// Build a map of which messages should get context based on position setting
|
||||||
|
// Key: message index in recentMessages, Value: context string
|
||||||
|
const contextInjectionMap = new Map();
|
||||||
|
|
||||||
|
if (historyPersistence?.enabled) {
|
||||||
|
// Find the last assistant message index (in recentMessages)
|
||||||
|
let lastAssistantIdx = -1;
|
||||||
|
for (let i = recentMessages.length - 1; i >= 0; i--) {
|
||||||
|
if (!recentMessages[i].is_user && !recentMessages[i].is_system) {
|
||||||
|
lastAssistantIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate through assistant messages to find tracker data
|
||||||
|
for (let i = 0; i < recentMessages.length; i++) {
|
||||||
|
const message = recentMessages[i];
|
||||||
|
|
||||||
|
// Skip user and system messages - only assistant messages have tracker data
|
||||||
|
if (message.is_user || message.is_system) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip the last assistant message - it gets current context elsewhere
|
||||||
|
if (i === lastAssistantIdx) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the rpg_companion_swipes data for current swipe
|
||||||
|
// Data can be in two places:
|
||||||
|
// 1. message.extra.rpg_companion_swipes (current session, before save)
|
||||||
|
// 2. message.swipe_info[swipeId].extra.rpg_companion_swipes (loaded from file)
|
||||||
|
const currentSwipeId = message.swipe_id || 0;
|
||||||
|
let swipeData = message.extra?.rpg_companion_swipes;
|
||||||
|
|
||||||
|
// If not in message.extra, check swipe_info
|
||||||
|
if (!swipeData && message.swipe_info && message.swipe_info[currentSwipeId]) {
|
||||||
|
swipeData = message.swipe_info[currentSwipeId].extra?.rpg_companion_swipes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!swipeData) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackerData = swipeData[currentSwipeId];
|
||||||
|
if (!trackerData) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Refresh RPG Info, use sendAllEnabledOnRefresh setting
|
||||||
|
// When true, include all enabled stats from preset instead of only persistInHistory stats
|
||||||
|
const useAllEnabled = historyPersistence.sendAllEnabledOnRefresh === true;
|
||||||
|
const formattedContext = formatHistoricalTrackerData(trackerData, trackerConfig, userName, useAllEnabled);
|
||||||
|
if (!formattedContext) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preamble = historyPersistence.contextPreamble || 'Context for that moment:';
|
||||||
|
const wrappedContext = `\n${preamble}\n${formattedContext}`;
|
||||||
|
|
||||||
|
// Determine target message based on position
|
||||||
|
let targetIdx = i;
|
||||||
|
|
||||||
|
if (position === 'user_message_end') {
|
||||||
|
// Find the preceding user message before this assistant message
|
||||||
|
// This is the user message that prompted this assistant response
|
||||||
|
for (let j = i - 1; j >= 0; j--) {
|
||||||
|
if (recentMessages[j].is_user && !recentMessages[j].is_system) {
|
||||||
|
targetIdx = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no user message found before, skip
|
||||||
|
if (targetIdx === i) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For assistant_message_end: inject into the assistant message itself
|
||||||
|
|
||||||
|
// Append to existing or create new entry
|
||||||
|
if (contextInjectionMap.has(targetIdx)) {
|
||||||
|
contextInjectionMap.set(targetIdx, contextInjectionMap.get(targetIdx) + wrappedContext);
|
||||||
|
} else {
|
||||||
|
contextInjectionMap.set(targetIdx, wrappedContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now build the messages array with injected context
|
||||||
|
for (let i = 0; i < recentMessages.length; i++) {
|
||||||
|
const message = recentMessages[i];
|
||||||
|
let content = message.mes;
|
||||||
|
|
||||||
|
// Add historical context if this message is a target
|
||||||
|
if (contextInjectionMap.has(i)) {
|
||||||
|
content += contextInjectionMap.get(i);
|
||||||
|
}
|
||||||
|
|
||||||
for (const message of recentMessages) {
|
|
||||||
messages.push({
|
messages.push({
|
||||||
role: message.is_user ? 'user' : 'assistant',
|
role: message.is_user ? 'user' : 'assistant',
|
||||||
content: message.mes
|
content: content
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { chat, user_avatar, setExtensionPrompt, extension_prompt_types, saveChatDebounced } from '../../../../../../../script.js';
|
import { chat, chat_metadata, user_avatar, setExtensionPrompt, extension_prompt_types } from '../../../../../../../script.js';
|
||||||
|
|
||||||
// Core modules
|
// Core modules
|
||||||
import {
|
import {
|
||||||
@@ -13,14 +13,29 @@ import {
|
|||||||
committedTrackerData,
|
committedTrackerData,
|
||||||
lastActionWasSwipe,
|
lastActionWasSwipe,
|
||||||
isPlotProgression,
|
isPlotProgression,
|
||||||
|
isAwaitingNewMessage,
|
||||||
setLastActionWasSwipe,
|
setLastActionWasSwipe,
|
||||||
setIsPlotProgression,
|
setIsPlotProgression,
|
||||||
setIsGenerating,
|
setIsGenerating,
|
||||||
|
setIsAwaitingNewMessage,
|
||||||
updateLastGeneratedData,
|
updateLastGeneratedData,
|
||||||
updateCommittedTrackerData,
|
updateCommittedTrackerData,
|
||||||
$musicPlayerContainer
|
$musicPlayerContainer,
|
||||||
|
incrementSeparateGenerationId
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveChatData, loadChatData } from '../../core/persistence.js';
|
import {
|
||||||
|
saveChatData,
|
||||||
|
loadChatData,
|
||||||
|
autoSwitchPresetForEntity,
|
||||||
|
getMessageSwipeTrackerData,
|
||||||
|
getCurrentMessageSwipeTrackerData,
|
||||||
|
restoreLatestTrackerStateFromChat,
|
||||||
|
setMessageSwipeTrackerData,
|
||||||
|
getSwipeData,
|
||||||
|
commitTrackerDataFromPriorMessage,
|
||||||
|
inheritSwipeDataFromPriorMessage,
|
||||||
|
flushDeferredChatDataSave
|
||||||
|
} from '../../core/persistence.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
// Generation & Parsing
|
// Generation & Parsing
|
||||||
@@ -28,23 +43,69 @@ import { parseResponse, parseUserStats } from '../generation/parser.js';
|
|||||||
import { parseAndStoreSpotifyUrl, convertToEmbedUrl } from '../features/musicPlayer.js';
|
import { parseAndStoreSpotifyUrl, convertToEmbedUrl } from '../features/musicPlayer.js';
|
||||||
import { updateRPGData } from '../generation/apiClient.js';
|
import { updateRPGData } from '../generation/apiClient.js';
|
||||||
import { removeLocks } from '../generation/lockManager.js';
|
import { removeLocks } from '../generation/lockManager.js';
|
||||||
import { onGenerationStarted } from '../generation/injector.js';
|
import { onGenerationStarted, initHistoryInjectionListeners } from '../generation/injector.js';
|
||||||
|
|
||||||
// Rendering
|
// Rendering
|
||||||
import { renderUserStats } from '../rendering/userStats.js';
|
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';
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||||||
|
|
||||||
|
// UI
|
||||||
|
import { setFabLoadingState, updateFabWidgets } from '../ui/mobile.js';
|
||||||
|
import { updateStripWidgets } from '../ui/desktop.js';
|
||||||
|
|
||||||
// Chapter checkpoint
|
// Chapter checkpoint
|
||||||
import { updateAllCheckpointIndicators } from '../ui/checkpointUI.js';
|
import { updateAllCheckpointIndicators } from '../ui/checkpointUI.js';
|
||||||
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
||||||
|
|
||||||
|
let chatStateRehydrateRunId = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the swipe store of the last assistant message in `currentChat` and
|
||||||
|
* writes its data into `lastGeneratedData`, including syncing stat bars via
|
||||||
|
* `parseUserStats`. If no assistant message exists, or none has stored swipe
|
||||||
|
* data, `lastGeneratedData` is left unchanged.
|
||||||
|
*
|
||||||
|
* Use this wherever the displayed tracker state must be re-derived from the
|
||||||
|
* authoritative swipe store rather than from chat_metadata (e.g. after a
|
||||||
|
* CHAT_CHANGED caused by branching, or after a message deletion).
|
||||||
|
*
|
||||||
|
* @param {Array} currentChat - Live chat array from getContext().chat
|
||||||
|
* @returns {boolean} True if swipe data was found and applied
|
||||||
|
*/
|
||||||
|
function syncLastGeneratedDataFromSwipeStore(currentChat) {
|
||||||
|
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||||||
|
const msg = currentChat[i];
|
||||||
|
if (!msg.is_user && !msg.is_system) {
|
||||||
|
const swipeId = msg.swipe_id || 0;
|
||||||
|
const swipeData = getSwipeData(msg, swipeId);
|
||||||
|
if (swipeData) {
|
||||||
|
lastGeneratedData.userStats = swipeData.userStats || null;
|
||||||
|
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
||||||
|
// Normalize characterThoughts to string (backward compat with old object format).
|
||||||
|
if (swipeData.characterThoughts && typeof swipeData.characterThoughts === 'object') {
|
||||||
|
lastGeneratedData.characterThoughts = JSON.stringify(swipeData.characterThoughts, null, 2);
|
||||||
|
} else {
|
||||||
|
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
||||||
|
}
|
||||||
|
if (swipeData.userStats) {
|
||||||
|
parseUserStats(swipeData.userStats);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false; // Last assistant message exists but has no swipe data yet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false; // No assistant messages in chat
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Commits the tracker data from the last assistant message to be used as source for next generation.
|
* Commits the tracker data from the last assistant message to be used as source for next generation.
|
||||||
* This should be called when the user has replied to a message, ensuring all swipes of the next
|
* This should be called when the user has replied to a message, ensuring all swipes of the next
|
||||||
@@ -59,28 +120,299 @@ export function commitTrackerData() {
|
|||||||
// Find the last assistant message
|
// Find the last assistant message
|
||||||
for (let i = chat.length - 1; i >= 0; i--) {
|
for (let i = chat.length - 1; i >= 0; i--) {
|
||||||
const message = chat[i];
|
const message = chat[i];
|
||||||
if (!message.is_user) {
|
if (!message.is_user && !message.is_system) {
|
||||||
// Found last assistant message - commit its tracker data
|
// Found last assistant message - commit its tracker data
|
||||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
const swipeId = message.swipe_id || 0;
|
||||||
const swipeId = message.swipe_id || 0;
|
const swipeData = getSwipeData(message, swipeId);
|
||||||
const swipeData = message.extra.rpg_companion_swipes[swipeId];
|
|
||||||
|
|
||||||
if (swipeData) {
|
if (swipeData) {
|
||||||
// console.log('[RPG Companion] Committing tracker data from assistant message at index', i, 'swipe', swipeId);
|
// console.log('[RPG Companion] Committing tracker data from assistant message at index', i, 'swipe', swipeId);
|
||||||
committedTrackerData.userStats = swipeData.userStats || null;
|
committedTrackerData.userStats = swipeData.userStats || null;
|
||||||
committedTrackerData.infoBox = swipeData.infoBox || null;
|
committedTrackerData.infoBox = swipeData.infoBox || null;
|
||||||
committedTrackerData.characterThoughts = swipeData.characterThoughts || null;
|
const rawCharacterThoughts = swipeData.characterThoughts;
|
||||||
|
if (rawCharacterThoughts == null) {
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
} else if (typeof rawCharacterThoughts === 'object') {
|
||||||
|
committedTrackerData.characterThoughts = JSON.stringify(rawCharacterThoughts);
|
||||||
} else {
|
} else {
|
||||||
// console.log('[RPG Companion] No swipe data found for swipe', swipeId);
|
committedTrackerData.characterThoughts = String(rawCharacterThoughts);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// console.log('[RPG Companion] No RPG data found in last assistant message');
|
// No saved swipe data — treat as empty (e.g. first message, no prior generation)
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSwipeTrackerData(message) {
|
||||||
|
return getMessageSwipeTrackerData(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSwipeTrackerData(message) {
|
||||||
|
return getCurrentMessageSwipeTrackerData(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAssistantMessageBody() {
|
||||||
|
const $messages = $('#chat .mes');
|
||||||
|
|
||||||
|
for (let i = $messages.length - 1; i >= 0; i--) {
|
||||||
|
const $message = $messages.eq(i);
|
||||||
|
if ($message.attr('is_user') === 'true') continue;
|
||||||
|
if ($message.find('.mes_text').length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAnyTrackerStateInChat() {
|
||||||
|
const chatMessages = getContext()?.chat || [];
|
||||||
|
|
||||||
|
for (let i = chatMessages.length - 1; i >= 0; i--) {
|
||||||
|
const swipeData = getSwipeTrackerData(chatMessages[i]);
|
||||||
|
if (swipeData?.userStats || swipeData?.infoBox || swipeData?.characterThoughts) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasAssistantMessagesInChat() {
|
||||||
|
const chatMessages = getContext()?.chat || [];
|
||||||
|
return chatMessages.some(message => message && !message.is_user && !message.is_system);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPotentialTrackerSourceInChat() {
|
||||||
|
const chatMessages = getContext()?.chat || [];
|
||||||
|
|
||||||
|
for (const message of chatMessages) {
|
||||||
|
if (!message || message.is_user || message.is_system) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.extra?.rpg_companion_swipes) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(message.swipe_info) && message.swipe_info.some(info => info?.extra?.rpg_companion_swipes)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(message.swipes) && message.swipes.length > 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRehydrateUserStatsFromDisplayData() {
|
||||||
|
const hasSavedUserStats = !!chat_metadata?.rpg_companion?.userStats;
|
||||||
|
if (!hasSavedUserStats && lastGeneratedData.userStats) {
|
||||||
|
try {
|
||||||
|
parseUserStats(lastGeneratedData.userStats);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[RPG Companion] Failed to rebuild user stats from display data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSwipeText(message) {
|
||||||
|
const swipeId = Number(message?.swipe_id ?? 0);
|
||||||
|
|
||||||
|
if (Array.isArray(message?.swipes) && typeof message.swipes[swipeId] === 'string' && message.swipes[swipeId].trim()) {
|
||||||
|
return message.swipes[swipeId];
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeof message?.mes === 'string' ? message.mes : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the currently active swipe index for a message.
|
||||||
|
* Some ST flows can briefly expose a stale message.swipe_id during swipe transitions,
|
||||||
|
* so we also match against message.mes in the swipes array when possible.
|
||||||
|
*
|
||||||
|
* @param {Object} message - Assistant message object
|
||||||
|
* @returns {number} Active swipe index
|
||||||
|
*/
|
||||||
|
function resolveActiveSwipeId(message) {
|
||||||
|
const fallbackSwipeId = Number(message?.swipe_id ?? 0);
|
||||||
|
const swipes = Array.isArray(message?.swipes) ? message.swipes : null;
|
||||||
|
|
||||||
|
if (!swipes || swipes.length === 0) {
|
||||||
|
return Math.max(0, fallbackSwipeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentText = typeof message?.mes === 'string' ? message.mes : '';
|
||||||
|
if (currentText) {
|
||||||
|
for (let i = swipes.length - 1; i >= 0; i--) {
|
||||||
|
if (typeof swipes[i] === 'string' && swipes[i] === currentText) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallbackSwipeId < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(fallbackSwipeId, swipes.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function repairLatestTrackerStateFromCurrentSwipeContent(chatMessages = getContext()?.chat || []) {
|
||||||
|
for (let i = chatMessages.length - 1; i >= 0; i--) {
|
||||||
|
const message = chatMessages[i];
|
||||||
|
if (!message || message.is_user || message.is_system) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipeId = Number(message.swipe_id ?? 0);
|
||||||
|
if (getCurrentSwipeTrackerData(message)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSwipeText = getCurrentSwipeText(message);
|
||||||
|
if (!currentSwipeText) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedData = parseResponse(currentSwipeText, { suppressNoDataError: true });
|
||||||
|
if (parsedData.userStats) {
|
||||||
|
parsedData.userStats = removeLocks(parsedData.userStats);
|
||||||
|
}
|
||||||
|
if (parsedData.infoBox) {
|
||||||
|
parsedData.infoBox = removeLocks(parsedData.infoBox);
|
||||||
|
}
|
||||||
|
if (parsedData.characterThoughts) {
|
||||||
|
parsedData.characterThoughts = removeLocks(parsedData.characterThoughts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsedData.userStats && !parsedData.infoBox && !parsedData.characterThoughts) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessageSwipeTrackerData(message, swipeId, {
|
||||||
|
userStats: parsedData.userStats || null,
|
||||||
|
infoBox: parsedData.infoBox || null,
|
||||||
|
characterThoughts: parsedData.characterThoughts || null
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreOrRepairLatestTrackerState() {
|
||||||
|
const chatMessages = getContext()?.chat || [];
|
||||||
|
let restored = restoreLatestTrackerStateFromChat(chatMessages);
|
||||||
|
|
||||||
|
if (!restored) {
|
||||||
|
const repaired = repairLatestTrackerStateFromCurrentSwipeContent(chatMessages);
|
||||||
|
if (repaired) {
|
||||||
|
restored = restoreLatestTrackerStateFromChat(chatMessages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return restored;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rerenderRpgState() {
|
||||||
|
renderUserStats();
|
||||||
|
renderInfoBox();
|
||||||
|
renderThoughts();
|
||||||
|
renderInventory();
|
||||||
|
renderEquipment();
|
||||||
|
renderQuests();
|
||||||
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scheduleChatStateRehydration() {
|
||||||
|
chatStateRehydrateRunId++;
|
||||||
|
const runId = chatStateRehydrateRunId;
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 15;
|
||||||
|
const eagerRetryAttempts = 4;
|
||||||
|
|
||||||
|
const tryRestoreState = () => {
|
||||||
|
if (runId !== chatStateRehydrateRunId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
loadChatData();
|
||||||
|
restoreOrRepairLatestTrackerState();
|
||||||
|
maybeRehydrateUserStatsFromDisplayData();
|
||||||
|
rerenderRpgState();
|
||||||
|
|
||||||
|
const hasRestoredTrackerState = !!(
|
||||||
|
lastGeneratedData.userStats
|
||||||
|
|| lastGeneratedData.infoBox
|
||||||
|
|| lastGeneratedData.characterThoughts
|
||||||
|
|| committedTrackerData.userStats
|
||||||
|
|| committedTrackerData.infoBox
|
||||||
|
|| committedTrackerData.characterThoughts
|
||||||
|
);
|
||||||
|
const hasStoredTrackerState = !!chat_metadata?.rpg_companion || hasAnyTrackerStateInChat();
|
||||||
|
const hasAssistantMessages = hasAssistantMessagesInChat();
|
||||||
|
const hasPotentialTrackerSource = hasPotentialTrackerSourceInChat();
|
||||||
|
const chatBodyReady = hasAssistantMessageBody();
|
||||||
|
|
||||||
|
if (chatBodyReady) {
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldRetryForRestore = !hasRestoredTrackerState && (
|
||||||
|
hasStoredTrackerState
|
||||||
|
|| (hasAssistantMessages && attempts < eagerRetryAttempts)
|
||||||
|
|| (hasPotentialTrackerSource && attempts < maxAttempts)
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldRetryForDom = !chatBodyReady && hasAssistantMessages;
|
||||||
|
|
||||||
|
if ((shouldRetryForRestore || shouldRetryForDom) && attempts < maxAttempts) {
|
||||||
|
setTimeout(tryRestoreState, 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(tryRestoreState, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onChatLoaded() {
|
||||||
|
loadChatData();
|
||||||
|
restoreOrRepairLatestTrackerState();
|
||||||
|
maybeRehydrateUserStatsFromDisplayData();
|
||||||
|
rerenderRpgState();
|
||||||
|
flushDeferredChatDataSave();
|
||||||
|
scheduleChatStateRehydration();
|
||||||
|
updateAllCheckpointIndicators();
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncDisplayedTrackerStateFromChat() {
|
||||||
|
const restored = restoreOrRepairLatestTrackerState();
|
||||||
|
|
||||||
|
if (!restored) {
|
||||||
|
lastGeneratedData.userStats = null;
|
||||||
|
lastGeneratedData.infoBox = null;
|
||||||
|
lastGeneratedData.characterThoughts = null;
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
rerenderRpgState();
|
||||||
|
updateChatThoughts();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event handler for when the user sends a message.
|
* Event handler for when the user sends a message.
|
||||||
* Sets the flag to indicate this is NOT a swipe.
|
* Sets the flag to indicate this is NOT a swipe.
|
||||||
@@ -97,6 +429,7 @@ export function onMessageSent() {
|
|||||||
const chat = context.chat;
|
const chat = context.chat;
|
||||||
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
const lastMessage = chat && chat.length > 0 ? chat[chat.length - 1] : null;
|
||||||
|
|
||||||
|
|
||||||
if (lastMessage && lastMessage.mes === '...') {
|
if (lastMessage && lastMessage.mes === '...') {
|
||||||
// console.log('[RPG Companion] 🟢 Ignoring onMessageSent: streaming placeholder message');
|
// console.log('[RPG Companion] 🟢 Ignoring onMessageSent: streaming placeholder message');
|
||||||
return;
|
return;
|
||||||
@@ -105,16 +438,16 @@ export function onMessageSent() {
|
|||||||
// console.log('[RPG Companion] 🟢 EVENT: onMessageSent (after placeholder check)');
|
// console.log('[RPG Companion] 🟢 EVENT: onMessageSent (after placeholder check)');
|
||||||
// console.log('[RPG Companion] 🟢 NOTE: lastActionWasSwipe will be reset in onMessageReceived after generation completes');
|
// console.log('[RPG Companion] 🟢 NOTE: lastActionWasSwipe will be reset in onMessageReceived after generation completes');
|
||||||
|
|
||||||
// For separate mode with auto-update disabled, commit displayed tracker
|
// Set flag to indicate we're expecting a new message from generation
|
||||||
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
|
// This allows auto-update to distinguish between new generations and loading chat history
|
||||||
if (lastGeneratedData.userStats || lastGeneratedData.infoBox || lastGeneratedData.characterThoughts) {
|
setIsAwaitingNewMessage(true);
|
||||||
committedTrackerData.userStats = lastGeneratedData.userStats;
|
|
||||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
|
||||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
|
||||||
|
|
||||||
// console.log('[RPG Companion] 💾 SEPARATE MODE: Committed displayed tracker (auto-update disabled)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
// Note: FAB spinning is NOT shown for together mode since no extra API request is made
|
||||||
|
// The RPG data comes embedded in the main response
|
||||||
|
// FAB spinning is handled by apiClient.js for separate/external modes when updateRPGData() is called
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,8 +470,9 @@ export async function onMessageReceived(data) {
|
|||||||
// Commit happens in onMessageSent (when user sends message, before generation)
|
// Commit happens in onMessageSent (when user sends message, before generation)
|
||||||
const lastMessage = chat[chat.length - 1];
|
const lastMessage = chat[chat.length - 1];
|
||||||
if (lastMessage && !lastMessage.is_user) {
|
if (lastMessage && !lastMessage.is_user) {
|
||||||
|
const rawSwipeId = Number(lastMessage.swipe_id ?? 0);
|
||||||
const responseText = lastMessage.mes;
|
const responseText = lastMessage.mes;
|
||||||
const parsedData = parseResponse(responseText);
|
const parsedData = parseResponse(responseText, { suppressNoDataError: true });
|
||||||
|
|
||||||
// Note: Don't show parsing error here - this event fires when loading chat history too
|
// Note: Don't show parsing error here - this event fires when loading chat history too
|
||||||
// Error notification is handled in apiClient.js for fresh generations only
|
// Error notification is handled in apiClient.js for fresh generations only
|
||||||
@@ -178,32 +512,29 @@ export async function onMessageReceived(data) {
|
|||||||
lastMessage.extra.rpg_companion_swipes = {};
|
lastMessage.extra.rpg_companion_swipes = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentSwipeId = lastMessage.swipe_id || 0;
|
const currentSwipeId = resolveActiveSwipeId(lastMessage);
|
||||||
lastMessage.extra.rpg_companion_swipes[currentSwipeId] = {
|
|
||||||
|
setMessageSwipeTrackerData(lastMessage, currentSwipeId, {
|
||||||
userStats: parsedData.userStats,
|
userStats: parsedData.userStats,
|
||||||
infoBox: parsedData.infoBox,
|
infoBox: parsedData.infoBox,
|
||||||
characterThoughts: parsedData.characterThoughts
|
characterThoughts: parsedData.characterThoughts
|
||||||
};
|
});
|
||||||
|
|
||||||
// console.log('[RPG Companion] Stored RPG data for swipe', currentSwipeId);
|
// console.log('[RPG Companion] Stored RPG data for swipe', currentSwipeId);
|
||||||
|
|
||||||
// Remove the tracker code blocks from the visible message
|
// Remove the tracker code blocks from the visible message
|
||||||
let cleanedMessage = responseText;
|
let cleanedMessage = responseText;
|
||||||
|
|
||||||
// Only remove trackers if saveTrackerHistory is disabled
|
// Note: JSON code blocks are hidden from display by regex script (but preserved in message data)
|
||||||
// When enabled, trackers are in <trackers> XML tags which SillyTavern auto-hides
|
|
||||||
if (!extensionSettings.saveTrackerHistory) {
|
|
||||||
// Note: JSON code blocks are hidden from display by regex script (but preserved in message data)
|
|
||||||
|
|
||||||
// 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, '');
|
||||||
cleanedMessage = cleanedMessage.replace(/```[^`]*?Info Box\s*\n\s*---[^`]*?```\s*/gi, '');
|
cleanedMessage = cleanedMessage.replace(/```[^`]*?Info Box\s*\n\s*---[^`]*?```\s*/gi, '');
|
||||||
cleanedMessage = cleanedMessage.replace(/```[^`]*?Present Characters\s*\n\s*---[^`]*?```\s*/gi, '');
|
cleanedMessage = cleanedMessage.replace(/```[^`]*?Present Characters\s*\n\s*---[^`]*?```\s*/gi, '');
|
||||||
// Remove any stray "---" dividers that might appear after the code blocks
|
// Remove any stray "---" dividers that might appear after the code blocks
|
||||||
cleanedMessage = cleanedMessage.replace(/^\s*---\s*$/gm, '');
|
cleanedMessage = cleanedMessage.replace(/^\s*---\s*$/gm, '');
|
||||||
// Clean up multiple consecutive newlines
|
// Clean up multiple consecutive newlines
|
||||||
cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n');
|
cleanedMessage = cleanedMessage.replace(/\n{3,}/g, '\n\n');
|
||||||
}
|
|
||||||
// Note: <trackers> XML tags are automatically hidden by SillyTavern
|
// Note: <trackers> XML tags are automatically hidden by SillyTavern
|
||||||
// Note: <Song - Artist/> tags are also automatically hidden by SillyTavern
|
// Note: <Song - Artist/> tags are also automatically hidden by SillyTavern
|
||||||
|
|
||||||
@@ -220,9 +551,14 @@ export async function onMessageReceived(data) {
|
|||||||
renderInfoBox();
|
renderInfoBox();
|
||||||
renderThoughts();
|
renderThoughts();
|
||||||
renderInventory();
|
renderInventory();
|
||||||
|
renderEquipment();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
|
|
||||||
|
// Update FAB widgets and strip widgets with newly parsed data
|
||||||
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
|
|
||||||
// Then update the DOM to reflect the cleaned message
|
// Then update the DOM to reflect the cleaned message
|
||||||
// Using updateMessageBlock to perform macro substitutions + regex formatting
|
// Using updateMessageBlock to perform macro substitutions + regex formatting
|
||||||
const messageId = chat.length - 1;
|
const messageId = chat.length - 1;
|
||||||
@@ -230,6 +566,11 @@ export async function onMessageReceived(data) {
|
|||||||
|
|
||||||
// console.log('[RPG Companion] Cleaned message, removed tracker code blocks from DOM');
|
// console.log('[RPG Companion] Cleaned message, removed tracker code blocks from DOM');
|
||||||
|
|
||||||
|
// Re-insert chat thoughts after SillyTavern finishes rerendering the cleaned message DOM.
|
||||||
|
if (parsedData.characterThoughts) {
|
||||||
|
setTimeout(() => updateChatThoughts(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
// Save to chat metadata
|
// Save to chat metadata
|
||||||
saveChatData();
|
saveChatData();
|
||||||
}
|
}
|
||||||
@@ -247,16 +588,37 @@ export async function onMessageReceived(data) {
|
|||||||
// Just render the music player
|
// Just render the music player
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When auto-update is disabled, no tracker API call will run for this message.
|
||||||
|
// Inherit the prior assistant message's tracker data into this swipe slot so that
|
||||||
|
// commitTrackerDataFromPriorMessage can find a valid state next turn instead of nulling everything.
|
||||||
|
// Inheritance does not overwrite existing data, so it's safe to call even if the condition misses an edge case.
|
||||||
|
if (!extensionSettings.autoUpdate || !isAwaitingNewMessage) {
|
||||||
|
inheritSwipeDataFromPriorMessage(lastMessage, chat.length - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger auto-update if enabled (for both separate and external modes)
|
// Trigger auto-update if enabled (for both separate and external modes)
|
||||||
if (extensionSettings.autoUpdate) {
|
// Only trigger if this is a newly generated message, not loading chat history
|
||||||
|
if (extensionSettings.autoUpdate && isAwaitingNewMessage) {
|
||||||
|
// Capture the current generation ID before the async gap so that any
|
||||||
|
// message deletion (or a newer generation) that increments the counter
|
||||||
|
// while the 500ms timer or the API call is in-flight will cause
|
||||||
|
// updateRPGData to discard its result rather than stomping the UI.
|
||||||
|
const genId = incrementSeparateGenerationId();
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory, genId);
|
||||||
|
// Update FAB widgets and strip widgets after separate/external mode update completes
|
||||||
|
setFabLoadingState(false);
|
||||||
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the awaiting flag after processing the message
|
||||||
|
setIsAwaitingNewMessage(false);
|
||||||
|
|
||||||
// Reset the swipe flag after generation completes
|
// Reset the swipe flag after generation completes
|
||||||
// This ensures that if the user swiped → auto-reply generated → flag is now cleared
|
// This ensures that if the user swiped → auto-reply generated → flag is now cleared
|
||||||
// so the next user message will be treated as a new message (not a swipe)
|
// so the next user message will be treated as a new message (not a swipe)
|
||||||
@@ -272,6 +634,11 @@ export async function onMessageReceived(data) {
|
|||||||
// console.log('[RPG Companion] Plot progression generation completed');
|
// console.log('[RPG Companion] Plot progression generation completed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop FAB loading state and update widgets
|
||||||
|
setFabLoadingState(false);
|
||||||
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
|
|
||||||
// Re-apply checkpoint in case SillyTavern unhid messages during generation
|
// Re-apply checkpoint in case SillyTavern unhid messages during generation
|
||||||
await restoreCheckpointOnLoad();
|
await restoreCheckpointOnLoad();
|
||||||
}
|
}
|
||||||
@@ -283,28 +650,45 @@ export function onCharacterChanged() {
|
|||||||
// Remove thought panel and icon when changing characters
|
// Remove thought panel and icon when changing characters
|
||||||
$('#rpg-thought-panel').remove();
|
$('#rpg-thought-panel').remove();
|
||||||
$('#rpg-thought-icon').remove();
|
$('#rpg-thought-icon').remove();
|
||||||
|
$('.rpg-inline-thoughts, .rpg-inline-thought').remove();
|
||||||
$('#chat').off('scroll.thoughtPanel');
|
$('#chat').off('scroll.thoughtPanel');
|
||||||
$(window).off('resize.thoughtPanel');
|
$(window).off('resize.thoughtPanel');
|
||||||
$(document).off('click.thoughtPanel');
|
$(document).off('click.thoughtPanel');
|
||||||
|
|
||||||
|
// Auto-switch to the preset associated with this character/group (if any)
|
||||||
|
const presetSwitched = autoSwitchPresetForEntity();
|
||||||
|
// if (presetSwitched) {
|
||||||
|
// console.log('[RPG Companion] Auto-switched preset for character');
|
||||||
|
// }
|
||||||
|
|
||||||
// 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
|
||||||
|
// loadChatData() may have just restored stale data from the parent chat.
|
||||||
|
// Override lastGeneratedData from the swipe store of the last assistant message.
|
||||||
|
// The message objects in the branch already carry their full swipe stores, making this authoritative.
|
||||||
|
// If no swipe data exists (e.g. branching at message 0, or a chat with no generations yet),
|
||||||
|
// null out lastGeneratedData and committedTrackerData so we don't display stale values from the parent chat.
|
||||||
|
const hadSwipeData = syncLastGeneratedDataFromSwipeStore(getContext().chat);
|
||||||
|
if (!hadSwipeData) {
|
||||||
|
lastGeneratedData.userStats = null;
|
||||||
|
lastGeneratedData.infoBox = null;
|
||||||
|
lastGeneratedData.characterThoughts = null;
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Don't call commitTrackerData() here - it would overwrite the loaded committedTrackerData
|
// Don't call commitTrackerData() here - it would overwrite the loaded committedTrackerData
|
||||||
// with data from the last message, which may be null/empty. The loaded committedTrackerData
|
// with data from the last message, which may be null/empty. The loaded committedTrackerData
|
||||||
// already contains the committed state from when we last left this chat.
|
// already contains the committed state from when we last left this chat.
|
||||||
// commitTrackerData() will be called naturally when new messages arrive.
|
// commitTrackerData() will be called naturally when new messages arrive.
|
||||||
|
|
||||||
// Re-render with the loaded data
|
// Re-render with the loaded data and retry once SillyTavern finishes restoring chat state.
|
||||||
renderUserStats();
|
rerenderRpgState();
|
||||||
renderInfoBox();
|
scheduleChatStateRehydration();
|
||||||
renderThoughts();
|
|
||||||
renderInventory();
|
|
||||||
renderQuests();
|
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
|
||||||
|
|
||||||
// Update chat thought overlays
|
|
||||||
updateChatThoughts();
|
|
||||||
|
|
||||||
// Update checkpoint indicators for the loaded chat
|
// Update checkpoint indicators for the loaded chat
|
||||||
updateAllCheckpointIndicators();
|
updateAllCheckpointIndicators();
|
||||||
@@ -328,7 +712,8 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentSwipeId = message.swipe_id || 0;
|
const currentSwipeId = resolveActiveSwipeId(message);
|
||||||
|
const swipeCount = Array.isArray(message.swipes) ? message.swipes.length : 0;
|
||||||
|
|
||||||
// Only set flag to true if this swipe will trigger a NEW generation
|
// Only set flag to true if this swipe will trigger a NEW generation
|
||||||
// Check if the swipe already exists (has content in the swipes array)
|
// Check if the swipe already exists (has content in the swipes array)
|
||||||
@@ -336,35 +721,49 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
message.swipes[currentSwipeId] !== undefined &&
|
message.swipes[currentSwipeId] !== undefined &&
|
||||||
message.swipes[currentSwipeId] !== null &&
|
message.swipes[currentSwipeId] !== null &&
|
||||||
message.swipes[currentSwipeId].length > 0;
|
message.swipes[currentSwipeId].length > 0;
|
||||||
|
const swipeData = getSwipeData(message, currentSwipeId);
|
||||||
|
const isPendingNewSwipe = currentSwipeId >= swipeCount;
|
||||||
|
|
||||||
if (!isExistingSwipe) {
|
if (!isExistingSwipe) {
|
||||||
// This is a NEW swipe that will trigger generation
|
// This is a NEW swipe that will trigger generation
|
||||||
setLastActionWasSwipe(true);
|
setLastActionWasSwipe(true);
|
||||||
|
setIsAwaitingNewMessage(true);
|
||||||
|
// Immediately commit context from the prior assistant message (N-1) so generation
|
||||||
|
// uses the world state before this message, not the last-viewed sibling swipe.
|
||||||
|
commitTrackerDataFromPriorMessage(messageIndex);
|
||||||
// console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true');
|
// console.log('[RPG Companion] 🔵 NEW swipe detected - Set lastActionWasSwipe = true');
|
||||||
} else {
|
} else {
|
||||||
// This is navigating to an EXISTING swipe - don't change the flag
|
// This is navigating to an EXISTING swipe - don't change the flag
|
||||||
// console.log('[RPG Companion] 🔵 EXISTING swipe navigation - lastActionWasSwipe unchanged =', lastActionWasSwipe);
|
// console.log('[RPG Companion] 🔵 EXISTING swipe navigation - lastActionWasSwipe unchanged =', lastActionWasSwipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isPendingNewSwipe) {
|
||||||
|
lastGeneratedData.characterThoughts = null;
|
||||||
|
}
|
||||||
|
|
||||||
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
|
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
|
||||||
|
|
||||||
// Load RPG data for this swipe
|
// Load saved swipe data for the active swipe only.
|
||||||
// lastGeneratedData is for DISPLAY, committedTrackerData is for GENERATION
|
// Using the current-swipe helper here avoids falling back to another
|
||||||
// It's safe to load swipe data into lastGeneratedData - it won't be committed due to !lastActionWasSwipe check
|
// stored swipe payload and showing stale tracker state.
|
||||||
if (message.extra && message.extra.rpg_companion_swipes && message.extra.rpg_companion_swipes[currentSwipeId]) {
|
if (swipeData) {
|
||||||
const swipeData = message.extra.rpg_companion_swipes[currentSwipeId];
|
|
||||||
|
|
||||||
// Load swipe data into lastGeneratedData for display (both modes)
|
// Load swipe data into lastGeneratedData for display (both modes)
|
||||||
lastGeneratedData.userStats = swipeData.userStats || null;
|
lastGeneratedData.userStats = swipeData.userStats || null;
|
||||||
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
lastGeneratedData.infoBox = swipeData.infoBox || null;
|
||||||
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
|
||||||
|
|
||||||
// Parse user stats if available
|
// Normalize characterThoughts to string format (for backward compatibility with old object format)
|
||||||
|
if (swipeData.characterThoughts && typeof swipeData.characterThoughts === 'object') {
|
||||||
|
lastGeneratedData.characterThoughts = JSON.stringify(swipeData.characterThoughts, null, 2);
|
||||||
|
} else {
|
||||||
|
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync extensionSettings.userStats so stat bars reflect this swipe
|
||||||
if (swipeData.userStats) {
|
if (swipeData.userStats) {
|
||||||
parseUserStats(swipeData.userStats);
|
parseUserStats(swipeData.userStats);
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('[RPG Companion] 🔄 Loaded swipe data into lastGeneratedData for display:', currentSwipeId);
|
// console.log('[RPG Companion] 🔄 Loaded swipe data for swipe:', currentSwipeId);
|
||||||
} else {
|
} else {
|
||||||
// console.log('[RPG Companion] ℹ️ No stored data for swipe:', currentSwipeId);
|
// console.log('[RPG Companion] ℹ️ No stored data for swipe:', currentSwipeId);
|
||||||
}
|
}
|
||||||
@@ -374,13 +773,48 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
renderInfoBox();
|
renderInfoBox();
|
||||||
renderThoughts();
|
renderThoughts();
|
||||||
renderInventory();
|
renderInventory();
|
||||||
|
renderEquipment();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
renderMusicPlayer($musicPlayerContainer[0]);
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
|
|
||||||
|
// Update widget strips with the newly loaded swipe data
|
||||||
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
|
|
||||||
// Update chat thought overlays
|
// Update chat thought overlays
|
||||||
updateChatThoughts();
|
updateChatThoughts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function onMessageDeleted() {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate any pending or in-flight separate-mode generation so
|
||||||
|
// its result is not applied to the (now-changed) chat tail.
|
||||||
|
incrementSeparateGenerationId();
|
||||||
|
|
||||||
|
const currentChat = getContext().chat || [];
|
||||||
|
let lastAssistantIndex = -1;
|
||||||
|
for (let i = currentChat.length - 1; i >= 0; i--) {
|
||||||
|
if (!currentChat[i].is_user && !currentChat[i].is_system) {
|
||||||
|
lastAssistantIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncDisplayedTrackerStateFromChat();
|
||||||
|
|
||||||
|
// After the display state has been rebuilt, restore generation context from
|
||||||
|
// the assistant message immediately before the new tail message so the next
|
||||||
|
// generation uses the correct N-1 tracker state.
|
||||||
|
if (lastAssistantIndex !== -1) {
|
||||||
|
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveChatData();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the persona avatar image when user switches personas
|
* Update the persona avatar image when user switches personas
|
||||||
*/
|
*/
|
||||||
@@ -443,3 +877,11 @@ export async function onGenerationEnded() {
|
|||||||
// Re-apply checkpoint if one exists
|
// Re-apply checkpoint if one exists
|
||||||
await restoreCheckpointOnLoad();
|
await restoreCheckpointOnLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize history injection event listeners.
|
||||||
|
* Should be called once during extension initialization.
|
||||||
|
*/
|
||||||
|
export function initHistoryInjection() {
|
||||||
|
initHistoryInjectionListeners();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,550 @@
|
|||||||
|
/**
|
||||||
|
* Thought-based Character Expressions for the below-chat Present Characters panel.
|
||||||
|
*
|
||||||
|
* Derives portrait expressions from the current Present Characters thoughts
|
||||||
|
* payload, while keeping SillyTavern's native Character Expressions widget
|
||||||
|
* independent from the below-chat panel.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
|
import {
|
||||||
|
extensionSettings,
|
||||||
|
thoughtBasedExpressionPortraits,
|
||||||
|
setThoughtBasedExpressionPortraits
|
||||||
|
} from '../../core/state.js';
|
||||||
|
import {
|
||||||
|
getCurrentMessageSwipeTrackerData,
|
||||||
|
saveChatData,
|
||||||
|
setMessageSwipeTrackerField
|
||||||
|
} from '../../core/persistence.js';
|
||||||
|
import { isUsableThoughtBasedExpressionSrc } from '../../utils/thoughtBasedExpressionPortraits.js';
|
||||||
|
import {
|
||||||
|
getPresentCharactersTrackerData,
|
||||||
|
parsePresentCharacters
|
||||||
|
} from '../../utils/presentCharacters.js';
|
||||||
|
import {
|
||||||
|
classifyExpressionText,
|
||||||
|
clearExpressionsCompatibilityCache,
|
||||||
|
getExpressionClassificationSettingsSignature,
|
||||||
|
getExpressionPortraitSettingsSignature,
|
||||||
|
getExpressionsSettingsSignature,
|
||||||
|
isExpressionsExtensionEnabled,
|
||||||
|
resolveSpriteFolderNameForCharacter,
|
||||||
|
resolveExpressionPortraitForCharacter
|
||||||
|
} from '../../utils/sillyTavernExpressions.js';
|
||||||
|
|
||||||
|
const OFF_SCENE_THOUGHT_PATTERN = /\b(not\s+(currently\s+)?(in|at|present|in\s+the)\s+(the\s+)?(scene|area|room|location|vicinity))\b|\b(off[\s-]?scene)\b|\b(not\s+present)\b|\b(absent)\b|\b(away\s+from\s+(the\s+)?scene)\b/i;
|
||||||
|
const CHAT_CHANGE_RETRY_DELAYS = [0, 80, 220, 500];
|
||||||
|
const REFRESH_DEBOUNCE_DELAY = 80;
|
||||||
|
const THOUGHT_BASED_EXPRESSIONS_CACHE_VERSION = 1;
|
||||||
|
const THOUGHT_BASED_EXPRESSIONS_CACHE_FIELD = 'thoughtBasedExpressions';
|
||||||
|
|
||||||
|
let hiddenExpressionStyleElement = null;
|
||||||
|
let thoughtBasedExpressionsRefreshHandler = null;
|
||||||
|
let scheduledRefreshTimer = null;
|
||||||
|
let activeRefreshRunId = 0;
|
||||||
|
let lastCompletedRefreshSignature = null;
|
||||||
|
let lastExpressionSettingsSignature = null;
|
||||||
|
|
||||||
|
function normalizeName(name) {
|
||||||
|
return String(name || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldHideNativeExpressionDisplay() {
|
||||||
|
return extensionSettings.enabled === true && extensionSettings.hideDefaultExpressionDisplay === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldUseThoughtBasedExpressions() {
|
||||||
|
return extensionSettings.enabled === true
|
||||||
|
&& extensionSettings.enableThoughtBasedExpressions === true
|
||||||
|
&& extensionSettings.showAlternatePresentCharactersPanel === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyThoughtBasedExpressionsConsumers() {
|
||||||
|
thoughtBasedExpressionsRefreshHandler?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHideStyleCss() {
|
||||||
|
return `
|
||||||
|
#expression-image,
|
||||||
|
#expression-holder,
|
||||||
|
.expression-holder,
|
||||||
|
[data-expression-container],
|
||||||
|
#expression-image img,
|
||||||
|
#expression-holder img,
|
||||||
|
.expression-holder img,
|
||||||
|
[data-expression-container] img {
|
||||||
|
position: absolute !important;
|
||||||
|
left: -10000px !important;
|
||||||
|
top: 0 !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideNativeExpressionDisplay() {
|
||||||
|
if (hiddenExpressionStyleElement?.isConnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleElement = document.createElement('style');
|
||||||
|
styleElement.id = 'rpg-hidden-native-expression-display-style';
|
||||||
|
styleElement.textContent = getHideStyleCss();
|
||||||
|
document.head.appendChild(styleElement);
|
||||||
|
hiddenExpressionStyleElement = styleElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNativeExpressionDisplay() {
|
||||||
|
if (hiddenExpressionStyleElement?.isConnected) {
|
||||||
|
hiddenExpressionStyleElement.remove();
|
||||||
|
} else {
|
||||||
|
document.getElementById('rpg-hidden-native-expression-display-style')?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
hiddenExpressionStyleElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNativeExpressionDisplayVisibility() {
|
||||||
|
if (shouldHideNativeExpressionDisplay()) {
|
||||||
|
hideNativeExpressionDisplay();
|
||||||
|
} else {
|
||||||
|
showNativeExpressionDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearScheduledRefresh() {
|
||||||
|
if (scheduledRefreshTimer !== null) {
|
||||||
|
clearTimeout(scheduledRefreshTimer);
|
||||||
|
scheduledRefreshTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stableStringify(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return `[${value.map(item => stableStringify(item)).join(',')}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeThoughtPayload(payload) {
|
||||||
|
if (!payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof payload === 'object') {
|
||||||
|
return stableStringify(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof payload !== 'string') {
|
||||||
|
return String(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = payload.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return stableStringify(JSON.parse(trimmed));
|
||||||
|
} catch {
|
||||||
|
return trimmed.replace(/\r\n/g, '\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExpressionLabel(label) {
|
||||||
|
return String(label || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function arePortraitMapsEqual(left, right) {
|
||||||
|
const leftKeys = Object.keys(left);
|
||||||
|
const rightKeys = Object.keys(right);
|
||||||
|
|
||||||
|
if (leftKeys.length !== rightKeys.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return leftKeys.every(key => left[key] === right[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyThoughtBasedExpressionPortraits(nextPortraits) {
|
||||||
|
if (arePortraitMapsEqual(thoughtBasedExpressionPortraits, nextPortraits)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setThoughtBasedExpressionPortraits(nextPortraits);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function purgeInvalidThoughtBasedExpressionPortraits() {
|
||||||
|
const nextPortraits = {};
|
||||||
|
|
||||||
|
for (const [characterName, src] of Object.entries(thoughtBasedExpressionPortraits)) {
|
||||||
|
if (isUsableThoughtBasedExpressionSrc(src)) {
|
||||||
|
nextPortraits[characterName] = src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return applyThoughtBasedExpressionPortraits(nextPortraits);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMessageThoughtPayload(message) {
|
||||||
|
if (!message || message.is_user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipeData = getCurrentMessageSwipeTrackerData(message);
|
||||||
|
return normalizeThoughtPayload(swipeData?.characterThoughts ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findThoughtSourceMessageInfo(characterThoughtsData) {
|
||||||
|
const chatMessages = getContext()?.chat || [];
|
||||||
|
const currentThoughts = normalizeThoughtPayload(characterThoughtsData);
|
||||||
|
let fallback = null;
|
||||||
|
|
||||||
|
for (let i = chatMessages.length - 1; i >= 0; i--) {
|
||||||
|
const message = chatMessages[i];
|
||||||
|
if (!message || message.is_user || message.is_system) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const swipeData = getCurrentMessageSwipeTrackerData(message);
|
||||||
|
if (!swipeData) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceInfo = {
|
||||||
|
message,
|
||||||
|
messageIndex: i,
|
||||||
|
swipeId: Number(message.swipe_id ?? 0),
|
||||||
|
swipeData
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!fallback) {
|
||||||
|
fallback = sourceInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageThoughts = getMessageThoughtPayload(message);
|
||||||
|
if (currentThoughts && messageThoughts === currentThoughts) {
|
||||||
|
return sourceInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentThoughts ? null : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isThoughtBasedExpressionsCache(candidate) {
|
||||||
|
return !!(
|
||||||
|
candidate
|
||||||
|
&& typeof candidate === 'object'
|
||||||
|
&& !Array.isArray(candidate)
|
||||||
|
&& candidate.version === THOUGHT_BASED_EXPRESSIONS_CACHE_VERSION
|
||||||
|
&& candidate.entries
|
||||||
|
&& typeof candidate.entries === 'object'
|
||||||
|
&& !Array.isArray(candidate.entries)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSwipeThoughtBasedExpressionsCache(sourceInfo) {
|
||||||
|
const directCache = sourceInfo?.swipeData?.[THOUGHT_BASED_EXPRESSIONS_CACHE_FIELD];
|
||||||
|
return isThoughtBasedExpressionsCache(directCache) ? directCache : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function areThoughtBasedExpressionsCachesEqual(left, right) {
|
||||||
|
return stableStringify(left) === stableStringify(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThoughtBasedExpressionEntries(characterThoughtsData) {
|
||||||
|
const thoughtsConfig = extensionSettings.trackerConfig?.presentCharacters?.thoughts;
|
||||||
|
if (thoughtsConfig?.enabled === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!characterThoughtsData) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentCharacters = parsePresentCharacters(characterThoughtsData);
|
||||||
|
return presentCharacters
|
||||||
|
.map(character => ({
|
||||||
|
name: String(character?.name || '').trim(),
|
||||||
|
thought: String(character?.ThoughtsContent || '').trim()
|
||||||
|
}))
|
||||||
|
.filter(character => character.name && character.thought && !OFF_SCENE_THOUGHT_PATTERN.test(character.thought));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRefreshSignature(thoughtEntries, expressionsSettingsSignature) {
|
||||||
|
return JSON.stringify({
|
||||||
|
expressionsSettingsSignature,
|
||||||
|
thoughtEntries: thoughtEntries.map(entry => ({
|
||||||
|
name: normalizeName(entry.name),
|
||||||
|
thought: entry.thought,
|
||||||
|
spriteFolderName: resolveSpriteFolderNameForCharacter(entry.name)
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshThoughtBasedExpressions({ force = false } = {}) {
|
||||||
|
updateNativeExpressionDisplayVisibility();
|
||||||
|
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
showNativeExpressionDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldUseThoughtBasedExpressions()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
lastCompletedRefreshSignature = null;
|
||||||
|
lastExpressionSettingsSignature = null;
|
||||||
|
clearExpressionsCompatibilityCache();
|
||||||
|
const portraitsChanged = applyThoughtBasedExpressionPortraits({});
|
||||||
|
if (portraitsChanged) {
|
||||||
|
saveChatData();
|
||||||
|
}
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expressionsSettingsSignature = getExpressionsSettingsSignature();
|
||||||
|
if (expressionsSettingsSignature !== lastExpressionSettingsSignature) {
|
||||||
|
clearExpressionsCompatibilityCache();
|
||||||
|
lastExpressionSettingsSignature = expressionsSettingsSignature;
|
||||||
|
lastCompletedRefreshSignature = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const characterThoughtsData = getPresentCharactersTrackerData({ useCommittedFallback: true });
|
||||||
|
const thoughtEntries = getThoughtBasedExpressionEntries(characterThoughtsData);
|
||||||
|
const refreshSignature = buildRefreshSignature(thoughtEntries, expressionsSettingsSignature);
|
||||||
|
if (!force && refreshSignature === lastCompletedRefreshSignature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceInfo = findThoughtSourceMessageInfo(characterThoughtsData);
|
||||||
|
const cachedThoughtBasedExpressions = getSwipeThoughtBasedExpressionsCache(sourceInfo);
|
||||||
|
const cachedEntries = cachedThoughtBasedExpressions?.entries && typeof cachedThoughtBasedExpressions.entries === 'object' && !Array.isArray(cachedThoughtBasedExpressions.entries)
|
||||||
|
? cachedThoughtBasedExpressions.entries
|
||||||
|
: {};
|
||||||
|
const currentThoughtsSignature = normalizeThoughtPayload(characterThoughtsData);
|
||||||
|
const classificationSettingsSignature = getExpressionClassificationSettingsSignature();
|
||||||
|
const portraitSettingsSignature = getExpressionPortraitSettingsSignature();
|
||||||
|
const runId = ++activeRefreshRunId;
|
||||||
|
const nextPortraits = {};
|
||||||
|
const nextCacheEntries = {};
|
||||||
|
|
||||||
|
for (const entry of thoughtEntries) {
|
||||||
|
const portraitKey = normalizeName(entry.name);
|
||||||
|
if (!portraitKey) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const spriteFolderName = resolveSpriteFolderNameForCharacter(entry.name);
|
||||||
|
const cachedEntry = cachedEntries[portraitKey] && typeof cachedEntries[portraitKey] === 'object'
|
||||||
|
? cachedEntries[portraitKey]
|
||||||
|
: null;
|
||||||
|
const previousSrc = nextPortraits[portraitKey] || thoughtBasedExpressionPortraits[portraitKey] || null;
|
||||||
|
const canReuseExpression = cachedEntry
|
||||||
|
&& cachedEntry.thought === entry.thought
|
||||||
|
&& cachedEntry.classificationSettingsSignature === classificationSettingsSignature
|
||||||
|
&& cachedEntry.spriteFolderName === spriteFolderName
|
||||||
|
&& typeof cachedEntry.expression === 'string';
|
||||||
|
|
||||||
|
const expression = canReuseExpression
|
||||||
|
? normalizeExpressionLabel(cachedEntry.expression)
|
||||||
|
: normalizeExpressionLabel(await classifyExpressionText(entry.thought, { characterName: entry.name }));
|
||||||
|
if (runId !== activeRefreshRunId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canReusePortrait = cachedEntry
|
||||||
|
&& cachedEntry.thought === entry.thought
|
||||||
|
&& cachedEntry.expression === expression
|
||||||
|
&& cachedEntry.portraitSettingsSignature === portraitSettingsSignature
|
||||||
|
&& cachedEntry.spriteFolderName === spriteFolderName
|
||||||
|
&& cachedEntry.portraitResolved === true;
|
||||||
|
|
||||||
|
const portraitSrc = canReusePortrait
|
||||||
|
? (isUsableThoughtBasedExpressionSrc(cachedEntry.portraitSrc) ? cachedEntry.portraitSrc : null)
|
||||||
|
: await resolveExpressionPortraitForCharacter(entry.name, expression, { previousSrc });
|
||||||
|
if (runId !== activeRefreshRunId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUsableThoughtBasedExpressionSrc(portraitSrc)) {
|
||||||
|
nextPortraits[portraitKey] = portraitSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextCacheEntries[portraitKey] = {
|
||||||
|
name: entry.name,
|
||||||
|
thought: entry.thought,
|
||||||
|
spriteFolderName,
|
||||||
|
classificationSettingsSignature,
|
||||||
|
portraitSettingsSignature,
|
||||||
|
expression,
|
||||||
|
portraitSrc: isUsableThoughtBasedExpressionSrc(portraitSrc) ? portraitSrc : null,
|
||||||
|
portraitResolved: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runId !== activeRefreshRunId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cacheChanged = false;
|
||||||
|
if (sourceInfo) {
|
||||||
|
const nextCache = {
|
||||||
|
version: THOUGHT_BASED_EXPRESSIONS_CACHE_VERSION,
|
||||||
|
thoughtsSignature: currentThoughtsSignature,
|
||||||
|
entries: nextCacheEntries
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!areThoughtBasedExpressionsCachesEqual(cachedThoughtBasedExpressions, nextCache)) {
|
||||||
|
setMessageSwipeTrackerField(sourceInfo.message, sourceInfo.swipeId, THOUGHT_BASED_EXPRESSIONS_CACHE_FIELD, nextCache);
|
||||||
|
cacheChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastCompletedRefreshSignature = refreshSignature;
|
||||||
|
const portraitsChanged = applyThoughtBasedExpressionPortraits(nextPortraits);
|
||||||
|
if (portraitsChanged || cacheChanged) {
|
||||||
|
saveChatData();
|
||||||
|
}
|
||||||
|
if (portraitsChanged) {
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setThoughtBasedExpressionsRefreshHandler(handler) {
|
||||||
|
thoughtBasedExpressionsRefreshHandler = typeof handler === 'function' ? handler : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queueThoughtBasedExpressionsUpdate({ immediate = false, force = false } = {}) {
|
||||||
|
clearScheduledRefresh();
|
||||||
|
|
||||||
|
const runRefresh = () => {
|
||||||
|
refreshThoughtBasedExpressions({ force }).catch(error => {
|
||||||
|
console.warn('[RPG Companion] Thought-based expressions update failed:', error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (immediate) {
|
||||||
|
runRefresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduledRefreshTimer = setTimeout(() => {
|
||||||
|
scheduledRefreshTimer = null;
|
||||||
|
runRefresh();
|
||||||
|
}, REFRESH_DEBOUNCE_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initThoughtBasedExpressions() {
|
||||||
|
const purged = purgeInvalidThoughtBasedExpressionPortraits();
|
||||||
|
updateNativeExpressionDisplayVisibility();
|
||||||
|
|
||||||
|
if (purged) {
|
||||||
|
saveChatData();
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldUseThoughtBasedExpressions()) {
|
||||||
|
queueThoughtBasedExpressionsUpdate({ immediate: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onThoughtBasedExpressionsChatChanged() {
|
||||||
|
if (!extensionSettings.enabled) {
|
||||||
|
showNativeExpressionDisplay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearScheduledRefresh();
|
||||||
|
activeRefreshRunId += 1;
|
||||||
|
lastCompletedRefreshSignature = null;
|
||||||
|
lastExpressionSettingsSignature = null;
|
||||||
|
clearExpressionsCompatibilityCache();
|
||||||
|
|
||||||
|
const purged = purgeInvalidThoughtBasedExpressionPortraits();
|
||||||
|
if (purged) {
|
||||||
|
saveChatData();
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const delay of CHAT_CHANGE_RETRY_DELAYS) {
|
||||||
|
setTimeout(() => {
|
||||||
|
updateNativeExpressionDisplayVisibility();
|
||||||
|
if (shouldUseThoughtBasedExpressions()) {
|
||||||
|
queueThoughtBasedExpressionsUpdate({ immediate: true, force: true });
|
||||||
|
} else {
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onThoughtBasedExpressionsSettingChanged(enabled) {
|
||||||
|
updateNativeExpressionDisplayVisibility();
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
const purged = purgeInvalidThoughtBasedExpressionPortraits();
|
||||||
|
if (purged) {
|
||||||
|
saveChatData();
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldUseThoughtBasedExpressions()) {
|
||||||
|
queueThoughtBasedExpressionsUpdate({ immediate: true, force: true });
|
||||||
|
} else {
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearScheduledRefresh();
|
||||||
|
activeRefreshRunId += 1;
|
||||||
|
lastCompletedRefreshSignature = null;
|
||||||
|
lastExpressionSettingsSignature = null;
|
||||||
|
clearExpressionsCompatibilityCache();
|
||||||
|
notifyThoughtBasedExpressionsConsumers();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onAlternatePresentCharactersVisibilityChanged() {
|
||||||
|
updateNativeExpressionDisplayVisibility();
|
||||||
|
|
||||||
|
if (shouldUseThoughtBasedExpressions()) {
|
||||||
|
queueThoughtBasedExpressionsUpdate({ immediate: true, force: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearScheduledRefresh();
|
||||||
|
activeRefreshRunId += 1;
|
||||||
|
lastCompletedRefreshSignature = null;
|
||||||
|
lastExpressionSettingsSignature = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onHideDefaultExpressionDisplaySettingChanged(enabled) {
|
||||||
|
extensionSettings.hideDefaultExpressionDisplay = enabled === true;
|
||||||
|
updateNativeExpressionDisplayVisibility();
|
||||||
|
setTimeout(() => updateNativeExpressionDisplayVisibility(), 0);
|
||||||
|
setTimeout(() => updateNativeExpressionDisplayVisibility(), 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearThoughtBasedExpressionsCache() {
|
||||||
|
clearScheduledRefresh();
|
||||||
|
activeRefreshRunId += 1;
|
||||||
|
lastCompletedRefreshSignature = null;
|
||||||
|
lastExpressionSettingsSignature = null;
|
||||||
|
clearExpressionsCompatibilityCache();
|
||||||
|
showNativeExpressionDisplay();
|
||||||
|
}
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
/**
|
||||||
|
* Equipment Actions Module
|
||||||
|
* Handles all user interactions with the equipment system
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
||||||
|
import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
|
||||||
|
import { renderEquipment } from '../rendering/equipment.js';
|
||||||
|
import { renderUserStats } from '../rendering/userStats.js';
|
||||||
|
import { EQUIPMENT_CATEGORIES, escapeHtml } from '../equipment/constants.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given slot is currently occupied by any item
|
||||||
|
* @param {string} slotId - Slot to check
|
||||||
|
* @param {Array} items - Equipment items array
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function isSlotOccupied(slotId, items) {
|
||||||
|
return items.some(item => item.slot === slotId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the first available slot for a given equipment type
|
||||||
|
* @param {string} type - Equipment type (helmet, ring, accessory, etc.)
|
||||||
|
* @param {Array} items - Equipment items array
|
||||||
|
* @returns {string|null} Available slot ID or null if all full
|
||||||
|
*/
|
||||||
|
function findAvailableSlot(type, items) {
|
||||||
|
const category = EQUIPMENT_CATEGORIES[type];
|
||||||
|
if (!category) return null;
|
||||||
|
return category.slots.find(slot => !isSlotOccupied(slot, items)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the slot ID currently assigned to an item
|
||||||
|
* @param {Object} item - Equipment item
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
function getItemSlot(item) {
|
||||||
|
return item.slot || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert old specific slot type (e.g. 'ring3') to generic category (e.g. 'ring')
|
||||||
|
* @param {string} type - Equipment type
|
||||||
|
* @returns {string} Generic type
|
||||||
|
*/
|
||||||
|
function normalizeType(type) {
|
||||||
|
if (!type) return type;
|
||||||
|
if (type.startsWith('ring') && type !== 'ring') return 'ring';
|
||||||
|
if (type.startsWith('accessory') && type !== 'accessory') return 'accessory';
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate old item types in the entire items array (for v6 → v7 migration)
|
||||||
|
* @param {Array} items - Equipment items array
|
||||||
|
*/
|
||||||
|
function migrateItemTypes(items) {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type) {
|
||||||
|
item.type = normalizeType(item.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a unique ID for equipment items
|
||||||
|
* @returns {string} Unique ID
|
||||||
|
*/
|
||||||
|
function generateItemId() {
|
||||||
|
return 'eq_' + Date.now() + '_' + Math.random().toString(36).substr(2, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates lastGeneratedData and committedTrackerData to include current equipment state
|
||||||
|
*/
|
||||||
|
function updateEquipmentData() {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
const currentData = lastGeneratedData.userStats || committedTrackerData.userStats;
|
||||||
|
|
||||||
|
if (currentData) {
|
||||||
|
const trimmed = currentData.trim();
|
||||||
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const jsonData = JSON.parse(currentData);
|
||||||
|
if (jsonData && typeof jsonData === 'object') {
|
||||||
|
jsonData.equipment = JSON.parse(JSON.stringify(equipment));
|
||||||
|
const updatedJSON = JSON.stringify(jsonData, null, 2);
|
||||||
|
lastGeneratedData.userStats = updatedJSON;
|
||||||
|
committedTrackerData.userStats = updatedJSON;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG Equipment] Failed to parse JSON, falling back to text format:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: rebuild text format
|
||||||
|
const stats = extensionSettings.userStats;
|
||||||
|
const config = extensionSettings.trackerConfig?.userStats || {};
|
||||||
|
let text = '';
|
||||||
|
|
||||||
|
const enabledStats = config.customStats?.filter(stat => stat && stat.enabled && stat.name && stat.id) || [];
|
||||||
|
for (const stat of enabledStats) {
|
||||||
|
const value = stats[stat.id] !== undefined ? stats[stat.id] : 100;
|
||||||
|
text += `${stat.name}: ${value}%\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.statusSection?.enabled) {
|
||||||
|
if (config.statusSection.showMoodEmoji) {
|
||||||
|
text += `${stats.mood}: `;
|
||||||
|
}
|
||||||
|
text += `${stats.conditions || 'None'}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include equipment data in fallback
|
||||||
|
const equipped = equipment.items.filter(item => item.slot);
|
||||||
|
if (equipped.length > 0) {
|
||||||
|
text += `\nEquipment:\n`;
|
||||||
|
for (const item of equipped) {
|
||||||
|
const bonuses = Object.entries(item.stats || {})
|
||||||
|
.filter(([_, val]) => val > 0)
|
||||||
|
.map(([key, val]) => `${key.toUpperCase()}+${val}`)
|
||||||
|
.join(' ');
|
||||||
|
text += `- ${item.slot}: ${item.name}${bonuses ? ' (' + bonuses + ')' : ''}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastGeneratedData.userStats = text.trim();
|
||||||
|
committedTrackerData.userStats = text.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the equipment creation modal
|
||||||
|
*/
|
||||||
|
export function showCreateModal() {
|
||||||
|
const modalHtml = generateCreateModalHTML();
|
||||||
|
$('body').append(modalHtml);
|
||||||
|
|
||||||
|
const $modal = $('#rpg-equipment-modal');
|
||||||
|
$modal.hide().fadeIn(200);
|
||||||
|
|
||||||
|
$('#rpg-eq-name').focus();
|
||||||
|
|
||||||
|
initStatCheckboxes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the equipment edit modal
|
||||||
|
* @param {string} itemId - ID of the item to edit
|
||||||
|
*/
|
||||||
|
export function showEditModal(itemId) {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
const item = equipment.items.find(i => i.id === itemId);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const modalHtml = generateEditModalHTML(item);
|
||||||
|
$('body').append(modalHtml);
|
||||||
|
|
||||||
|
const $modal = $('#rpg-equipment-modal');
|
||||||
|
$modal.hide().fadeIn(200);
|
||||||
|
|
||||||
|
initStatCheckboxes();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the equipment modal
|
||||||
|
*/
|
||||||
|
export function closeModal() {
|
||||||
|
$('#rpg-equipment-modal').fadeOut(150, function() {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates HTML for the create modal
|
||||||
|
* @returns {string} Modal HTML
|
||||||
|
*/
|
||||||
|
function generateCreateModalHTML() {
|
||||||
|
const attributes = getAvailableAttributes();
|
||||||
|
const typeOptions = generateTypeOptions(null);
|
||||||
|
const statCheckboxes = generateStatCheckboxes(attributes, null);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div id="rpg-equipment-modal" class="rpg-equipment-modal" role="dialog" aria-modal="true">
|
||||||
|
<div class="rpg-equipment-modal-content">
|
||||||
|
<header class="rpg-equipment-modal-header">
|
||||||
|
<h3>
|
||||||
|
<i class="fa-solid fa-shield-halved"></i>
|
||||||
|
<span>${i18n.getTranslation('equipment.createItemTitle') || 'Create Equipment'}</span>
|
||||||
|
</h3>
|
||||||
|
<button id="rpg-equipment-modal-close" class="rpg-popup-close" type="button">
|
||||||
|
<i class="fa-solid fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div class="rpg-equipment-modal-body">
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label for="rpg-eq-name">${i18n.getTranslation('equipment.name') || 'Name'}:</label>
|
||||||
|
<input type="text" id="rpg-eq-name" class="rpg-input" maxlength="50" placeholder="${i18n.getTranslation('equipment.namePlaceholder') || 'Enter equipment name...'}" />
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label for="rpg-eq-type">${i18n.getTranslation('equipment.type') || 'Type'}:</label>
|
||||||
|
<select id="rpg-eq-type" class="rpg-select">${typeOptions}</select>
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label>${i18n.getTranslation('equipment.stats') || 'Stats'}:</label>
|
||||||
|
<div class="rpg-eq-stats-grid">${statCheckboxes}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label for="rpg-eq-description">${i18n.getTranslation('equipment.description') || 'Description'}:</label>
|
||||||
|
<textarea id="rpg-eq-description" class="rpg-textarea" maxlength="200" rows="2" placeholder="${i18n.getTranslation('equipment.descriptionPlaceholder') || 'Enter description (optional)...'}"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="rpg-equipment-modal-footer">
|
||||||
|
<button id="rpg-equipment-modal-cancel" class="rpg-btn-secondary" type="button">
|
||||||
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
|
</button>
|
||||||
|
<button id="rpg-equipment-modal-save" class="rpg-btn-primary" type="button">
|
||||||
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.save') || 'Save'}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates HTML for the edit modal
|
||||||
|
* @param {Object} item - The item to edit
|
||||||
|
* @returns {string} Modal HTML
|
||||||
|
*/
|
||||||
|
function generateEditModalHTML(item) {
|
||||||
|
const attributes = getAvailableAttributes();
|
||||||
|
const typeOptions = generateTypeOptions(item.type);
|
||||||
|
const statCheckboxes = generateStatCheckboxes(attributes, item);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div id="rpg-equipment-modal" class="rpg-equipment-modal" role="dialog" aria-modal="true">
|
||||||
|
<div class="rpg-equipment-modal-content">
|
||||||
|
<header class="rpg-equipment-modal-header">
|
||||||
|
<h3>
|
||||||
|
<i class="fa-solid fa-shield-halved"></i>
|
||||||
|
<span>${i18n.getTranslation('equipment.editItemTitle') || 'Edit Equipment'}</span>
|
||||||
|
</h3>
|
||||||
|
<button id="rpg-equipment-modal-close" class="rpg-popup-close" type="button">
|
||||||
|
<i class="fa-solid fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<div class="rpg-equipment-modal-body">
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label for="rpg-eq-name">${i18n.getTranslation('equipment.name') || 'Name'}:</label>
|
||||||
|
<input type="text" id="rpg-eq-name" class="rpg-input" maxlength="50" value="${escapeHtml(item.name)}" placeholder="${i18n.getTranslation('equipment.namePlaceholder') || 'Enter equipment name...'}" />
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label for="rpg-eq-type">${i18n.getTranslation('equipment.type') || 'Type'}:</label>
|
||||||
|
<select id="rpg-eq-type" class="rpg-select">${typeOptions}</select>
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label>${i18n.getTranslation('equipment.stats') || 'Stats'}:</label>
|
||||||
|
<div class="rpg-eq-stats-grid">${statCheckboxes}</div>
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-form-group">
|
||||||
|
<label for="rpg-eq-description">${i18n.getTranslation('equipment.description') || 'Description'}:</label>
|
||||||
|
<textarea id="rpg-eq-description" class="rpg-textarea" maxlength="200" rows="2" placeholder="${i18n.getTranslation('equipment.descriptionPlaceholder') || 'Enter description (optional)...'}">${escapeHtml(item.description || '')}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="rpg-equipment-modal-footer">
|
||||||
|
<button id="rpg-equipment-modal-cancel" class="rpg-btn-secondary" type="button">
|
||||||
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
|
</button>
|
||||||
|
<button id="rpg-equipment-modal-save" class="rpg-btn-primary" type="button" data-edit-id="${item.id}">
|
||||||
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.save') || 'Save'}
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates type dropdown options
|
||||||
|
* @param {string|null} selectedType - Currently selected type or null
|
||||||
|
* @returns {string} HTML options
|
||||||
|
*/
|
||||||
|
function generateTypeOptions(selectedType) {
|
||||||
|
const types = Object.entries(EQUIPMENT_CATEGORIES).map(([value, def]) => ({
|
||||||
|
value,
|
||||||
|
label: def.slots[0] === value
|
||||||
|
? def.slots[0].charAt(0).toUpperCase() + def.slots[0].slice(1)
|
||||||
|
: value.charAt(0).toUpperCase() + value.slice(1) + (def.slots.length > 1 ? ` (max ${def.maxEquipped})` : '')
|
||||||
|
}));
|
||||||
|
|
||||||
|
return types.map(type => {
|
||||||
|
const selected = selectedType === type.value ? 'selected' : '';
|
||||||
|
return `<option value="${type.value}" ${selected}>${type.label}</option>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates stat checkboxes HTML
|
||||||
|
* @param {string[]} attributes - Available attribute IDs
|
||||||
|
* @param {Object|null} item - Current item for editing or null
|
||||||
|
* @returns {string} HTML for stat checkboxes
|
||||||
|
*/
|
||||||
|
function generateStatCheckboxes(attributes, item) {
|
||||||
|
return attributes.map(attr => {
|
||||||
|
const checked = item && item.stats && item.stats[attr] > 0;
|
||||||
|
const val = checked ? item.stats[attr] : 1;
|
||||||
|
return `
|
||||||
|
<label class="rpg-eq-stat-checkbox">
|
||||||
|
<input type="checkbox" class="rpg-eq-stat-check" data-attr="${attr}" ${checked ? 'checked' : ''} />
|
||||||
|
<span class="rpg-eq-stat-check-label">${attr.toUpperCase()}</span>
|
||||||
|
<input type="number" class="rpg-eq-stat-value-input" data-attr="${attr}" value="${val}" min="1" max="20" ${!checked ? 'disabled' : ''} />
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets available RPG attribute IDs from config
|
||||||
|
* @returns {string[]} Array of attribute IDs
|
||||||
|
*/
|
||||||
|
function getAvailableAttributes() {
|
||||||
|
const config = extensionSettings.trackerConfig?.userStats || {};
|
||||||
|
const rpgAttributes = config.rpgAttributes || [];
|
||||||
|
return rpgAttributes
|
||||||
|
.filter(attr => attr && attr.enabled && attr.id)
|
||||||
|
.map(attr => attr.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes stat checkbox change handlers
|
||||||
|
*/
|
||||||
|
function initStatCheckboxes() {
|
||||||
|
$('.rpg-eq-stat-check').on('change', function() {
|
||||||
|
const $input = $(this).siblings('.rpg-eq-stat-value-input');
|
||||||
|
$input.prop('disabled', !$(this).prop('checked'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the equipment item from the modal form
|
||||||
|
*/
|
||||||
|
export function saveEquipmentItem() {
|
||||||
|
const name = $('#rpg-eq-name').val().trim();
|
||||||
|
const type = $('#rpg-eq-type').val();
|
||||||
|
const description = $('#rpg-eq-description').val().trim();
|
||||||
|
const editId = $('#rpg-equipment-modal-save').data('edit-id');
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
$('#rpg-eq-name').focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect stats
|
||||||
|
const stats = {};
|
||||||
|
$('.rpg-eq-stat-check:checked').each(function() {
|
||||||
|
const attr = $(this).data('attr');
|
||||||
|
const val = parseInt($(this).siblings('.rpg-eq-stat-value-input').val()) || 1;
|
||||||
|
stats[attr] = Math.max(1, Math.min(20, val));
|
||||||
|
});
|
||||||
|
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
|
||||||
|
// Migrate old types (ring1-ring10 -> ring, accessory1-3 -> accessory)
|
||||||
|
migrateItemTypes(equipment.items);
|
||||||
|
|
||||||
|
if (editId) {
|
||||||
|
// Edit existing item
|
||||||
|
const item = equipment.items.find(i => i.id === editId);
|
||||||
|
if (item) {
|
||||||
|
const wasEquipped = !!item.slot;
|
||||||
|
item.name = name;
|
||||||
|
item.type = normalizeType(type);
|
||||||
|
item.stats = stats;
|
||||||
|
item.description = description;
|
||||||
|
|
||||||
|
// If type changed and item was equipped, re-equip to valid slot for new type
|
||||||
|
if (wasEquipped) {
|
||||||
|
const newCategory = EQUIPMENT_CATEGORIES[item.type];
|
||||||
|
if (newCategory && !newCategory.slots.includes(item.slot)) {
|
||||||
|
// Old slot is invalid for new type, find a new one
|
||||||
|
const newSlot = findAvailableSlot(item.type, equipment.items.filter(i => i.id !== editId));
|
||||||
|
item.slot = newSlot || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new item
|
||||||
|
const newItem = {
|
||||||
|
id: generateItemId(),
|
||||||
|
name: name,
|
||||||
|
type: normalizeType(type),
|
||||||
|
stats: stats,
|
||||||
|
description: description,
|
||||||
|
slot: null
|
||||||
|
};
|
||||||
|
equipment.items.push(newItem);
|
||||||
|
|
||||||
|
// Auto-equip to first available slot
|
||||||
|
const availableSlot = findAvailableSlot(newItem.type, equipment.items);
|
||||||
|
if (availableSlot) {
|
||||||
|
newItem.slot = availableSlot;
|
||||||
|
} else {
|
||||||
|
console.warn(`[RPG Equipment] Created "${name}" but no available slot for type "${newItem.type}". Item added to inventory unequipped.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEquipmentData();
|
||||||
|
saveSettings();
|
||||||
|
saveChatData();
|
||||||
|
updateMessageSwipeData();
|
||||||
|
closeModal();
|
||||||
|
renderEquipment();
|
||||||
|
renderUserStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equips an item to its designated slot
|
||||||
|
* @param {string} itemId - ID of the item to equip
|
||||||
|
*/
|
||||||
|
export function equipItem(itemId) {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
const item = equipment.items.find(i => i.id === itemId);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
const type = normalizeType(item.type);
|
||||||
|
item.type = type;
|
||||||
|
|
||||||
|
const category = EQUIPMENT_CATEGORIES[type];
|
||||||
|
if (!category) return;
|
||||||
|
|
||||||
|
const availableSlot = findAvailableSlot(type, equipment.items);
|
||||||
|
if (!availableSlot) {
|
||||||
|
console.warn(`[RPG Equipment] No available slot for type "${type}". All ${EQUIPMENT_CATEGORIES[type].maxEquipped} slots are full.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.slot = availableSlot;
|
||||||
|
|
||||||
|
updateEquipmentData();
|
||||||
|
saveSettings();
|
||||||
|
saveChatData();
|
||||||
|
updateMessageSwipeData();
|
||||||
|
renderEquipment();
|
||||||
|
renderUserStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unequips an item from a slot
|
||||||
|
* @param {string} slotId - The slot to unequip from
|
||||||
|
*/
|
||||||
|
export function unequipItem(slotId) {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
const item = equipment.items.find(i => i.slot === slotId);
|
||||||
|
if (item) {
|
||||||
|
item.slot = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEquipmentData();
|
||||||
|
saveSettings();
|
||||||
|
saveChatData();
|
||||||
|
updateMessageSwipeData();
|
||||||
|
renderEquipment();
|
||||||
|
renderUserStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an equipment item
|
||||||
|
* @param {string} itemId - ID of the item to delete
|
||||||
|
*/
|
||||||
|
export function deleteItem(itemId) {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
|
||||||
|
// Unequip if currently equipped
|
||||||
|
const item = equipment.items.find(i => i.id === itemId);
|
||||||
|
if (item && item.slot) {
|
||||||
|
item.slot = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from items array
|
||||||
|
equipment.items = equipment.items.filter(i => i.id !== itemId);
|
||||||
|
|
||||||
|
updateEquipmentData();
|
||||||
|
saveSettings();
|
||||||
|
saveChatData();
|
||||||
|
updateMessageSwipeData();
|
||||||
|
renderEquipment();
|
||||||
|
renderUserStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates total equipment bonuses per attribute from currently equipped items
|
||||||
|
* @returns {Object} Map of attribute ID to total bonus value
|
||||||
|
*/
|
||||||
|
export function getEquipmentBonuses() {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
const bonuses = {};
|
||||||
|
const items = equipment.items || [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (!item.slot || !item.stats) continue;
|
||||||
|
|
||||||
|
for (const [attr, val] of Object.entries(item.stats)) {
|
||||||
|
if (val > 0) {
|
||||||
|
bonuses[attr] = (bonuses[attr] || 0) + val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bonuses;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes all event listeners for equipment interactions
|
||||||
|
*/
|
||||||
|
export function initEquipmentEventListeners() {
|
||||||
|
// Show create modal
|
||||||
|
$(document).on('click', '[data-action="show-create-modal"]', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
showCreateModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Equip item from inventory
|
||||||
|
$(document).on('click', '[data-action="equip"]', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const itemId = $(this).data('item-id');
|
||||||
|
equipItem(itemId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unequip item from slot
|
||||||
|
$(document).on('click', '[data-action="unequip"]', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const slot = $(this).closest('.rpg-equipment-slot').data('slot');
|
||||||
|
unequipItem(slot);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Edit item
|
||||||
|
$(document).on('click', '[data-action="edit-item"]', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const itemId = $(this).data('item-id');
|
||||||
|
showEditModal(itemId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete item
|
||||||
|
$(document).on('click', '[data-action="delete-item"]', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const itemId = $(this).data('item-id');
|
||||||
|
deleteItem(itemId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal close button
|
||||||
|
$(document).on('click', '#rpg-equipment-modal-close', closeModal);
|
||||||
|
|
||||||
|
// Modal cancel button
|
||||||
|
$(document).on('click', '#rpg-equipment-modal-cancel', closeModal);
|
||||||
|
|
||||||
|
// Modal save button
|
||||||
|
$(document).on('click', '#rpg-equipment-modal-save', saveEquipmentItem);
|
||||||
|
|
||||||
|
// Close modal on backdrop click
|
||||||
|
$(document).on('click', '#rpg-equipment-modal', function(e) {
|
||||||
|
if (e.target === this) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter key to save in modal inputs
|
||||||
|
$(document).on('keypress', '#rpg-eq-name', function(e) {
|
||||||
|
if (e.which === 13) {
|
||||||
|
e.preventDefault();
|
||||||
|
saveEquipmentItem();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* Equipment Rendering Module
|
||||||
|
* Handles UI rendering for the equipment grid and item creation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { extensionSettings, $equipmentContainer } from '../../core/state.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { EQUIPMENT_CATEGORIES, SLOTS_LIST, escapeHtml } from '../equipment/constants.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a single equipment slot
|
||||||
|
* @param {Object} slotDef - Slot definition from SLOTS_LIST
|
||||||
|
* @param {Object|null} item - The equipped item or null
|
||||||
|
* @returns {string} HTML for the slot
|
||||||
|
*/
|
||||||
|
function renderSlot(slotDef, item) {
|
||||||
|
const slotId = slotDef.id;
|
||||||
|
const slotName = i18n.getTranslation(`equipment.slots.${slotId}`) || slotId.replace(/(\d+)/, ' $1');
|
||||||
|
const equippedClass = item ? 'equipped' : '';
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
const statsText = Object.entries(item.stats || {})
|
||||||
|
.filter(([_, val]) => val > 0)
|
||||||
|
.map(([key, val]) => `<span class="rpg-eq-stat"><span class="rpg-eq-stat-label">${escapeHtml(key.toUpperCase())}</span><span class="rpg-eq-stat-value">+${val}</span></span>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="rpg-equipment-slot ${equippedClass}" data-slot="${slotId}" data-item-id="${item.id}">
|
||||||
|
<div class="rpg-equipment-slot-header">
|
||||||
|
<i class="fa-solid ${slotDef.icon}"></i>
|
||||||
|
<span class="rpg-equipment-slot-name">${escapeHtml(slotName)}</span>
|
||||||
|
<button class="rpg-equipment-unequip-btn" data-action="unequip" title="${i18n.getTranslation('equipment.unequip') || 'Unequip'}">
|
||||||
|
<i class="fa-solid fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-item-name">${escapeHtml(item.name)}</div>
|
||||||
|
${statsText ? `<div class="rpg-equipment-stats">${statsText}</div>` : ''}
|
||||||
|
${item.description ? `<div class="rpg-equipment-description">${escapeHtml(item.description)}</div>` : ''}
|
||||||
|
<div class="rpg-equipment-item-actions">
|
||||||
|
<button class="rpg-equipment-edit-btn" data-action="edit-item" data-item-id="${item.id}" title="${i18n.getTranslation('equipment.editItem') || 'Edit item'}">
|
||||||
|
<i class="fa-solid fa-pen"></i>
|
||||||
|
</button>
|
||||||
|
<button class="rpg-equipment-delete-btn" data-action="delete-item" data-item-id="${item.id}" title="${i18n.getTranslation('equipment.deleteItem') || 'Delete item'}">
|
||||||
|
<i class="fa-solid fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="rpg-equipment-slot" data-slot="${slotId}">
|
||||||
|
<div class="rpg-equipment-slot-header">
|
||||||
|
<i class="fa-solid ${slotDef.icon}"></i>
|
||||||
|
<span class="rpg-equipment-slot-name">${escapeHtml(slotName)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="rpg-equipment-empty">${i18n.getTranslation('equipment.emptySlot') || 'Empty'}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the full equipment section HTML
|
||||||
|
* @returns {string} Complete HTML for the equipment section
|
||||||
|
*/
|
||||||
|
function generateEquipmentHTML() {
|
||||||
|
const equipment = extensionSettings.userStats.equipment;
|
||||||
|
const slots = equipment.slots || {};
|
||||||
|
const items = equipment.items || [];
|
||||||
|
|
||||||
|
let html = '<div class="rpg-equipment-container">';
|
||||||
|
|
||||||
|
// Header with add button
|
||||||
|
html += `
|
||||||
|
<div class="rpg-equipment-header">
|
||||||
|
<h3>
|
||||||
|
<i class="fa-solid fa-shield-halved"></i>
|
||||||
|
<span data-i18n-key="equipment.title">${i18n.getTranslation('equipment.title') || 'Equipment'}</span>
|
||||||
|
</h3>
|
||||||
|
<button class="rpg-equipment-add-btn" data-action="show-create-modal" title="${i18n.getTranslation('equipment.createItem') || 'Create new equipment'}">
|
||||||
|
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('equipment.createItem') || 'Create Equipment'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Equipment grid
|
||||||
|
html += '<div class="rpg-equipment-grid">';
|
||||||
|
|
||||||
|
// Render each slot
|
||||||
|
for (const slotDef of SLOTS_LIST) {
|
||||||
|
const item = items.find(i => i.slot === slotDef.id);
|
||||||
|
html += renderSlot(slotDef, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Inventory list (items not currently equipped)
|
||||||
|
const unequipped = items.filter(item => !item.slot);
|
||||||
|
if (unequipped.length > 0) {
|
||||||
|
html += '<div class="rpg-equipment-inventory">';
|
||||||
|
html += `<h4>${i18n.getTranslation('equipment.inventoryTitle') || 'Inventory'}</h4>`;
|
||||||
|
html += '<div class="rpg-equipment-inventory-list">';
|
||||||
|
|
||||||
|
for (const item of unequipped) {
|
||||||
|
const category = EQUIPMENT_CATEGORIES[item.type];
|
||||||
|
const statsText = Object.entries(item.stats || {})
|
||||||
|
.filter(([_, val]) => val > 0)
|
||||||
|
.map(([key, val]) => `<span class="rpg-eq-stat"><span class="rpg-eq-stat-label">${escapeHtml(key.toUpperCase())}</span><span class="rpg-eq-stat-value">+${val}</span></span>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="rpg-equipment-inventory-item" data-item-id="${item.id}">
|
||||||
|
<div class="rpg-equipment-inventory-item-header">
|
||||||
|
<i class="fa-solid ${category ? category.icon : 'fa-circle'}"></i>
|
||||||
|
<span class="rpg-equipment-inventory-item-name">${escapeHtml(item.name)}</span>
|
||||||
|
<span class="rpg-equipment-inventory-item-type">${category ? (i18n.getTranslation(`equipment.types.${item.type}`) || item.type) : escapeHtml(item.type)}</span>
|
||||||
|
</div>
|
||||||
|
${statsText ? `<div class="rpg-equipment-stats">${statsText}</div>` : ''}
|
||||||
|
<div class="rpg-equipment-item-actions">
|
||||||
|
<button class="rpg-equipment-equip-btn" data-action="equip" data-item-id="${item.id}" title="${i18n.getTranslation('equipment.equip') || 'Equip'}">
|
||||||
|
<i class="fa-solid fa-hand"></i>
|
||||||
|
</button>
|
||||||
|
<button class="rpg-equipment-edit-btn" data-action="edit-item" data-item-id="${item.id}" title="${i18n.getTranslation('equipment.editItem') || 'Edit item'}">
|
||||||
|
<i class="fa-solid fa-pen"></i>
|
||||||
|
</button>
|
||||||
|
<button class="rpg-equipment-delete-btn" data-action="delete-item" data-item-id="${item.id}" title="${i18n.getTranslation('equipment.deleteItem') || 'Delete item'}">
|
||||||
|
<i class="fa-solid fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main equipment rendering function
|
||||||
|
* Gets data from state/settings and updates DOM directly.
|
||||||
|
*/
|
||||||
|
export function renderEquipment() {
|
||||||
|
if (!$equipmentContainer || !extensionSettings.showEquipment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = generateEquipmentHTML();
|
||||||
|
$equipmentContainer.html(html);
|
||||||
|
|
||||||
|
// Re-apply translations
|
||||||
|
i18n.applyTranslations($equipmentContainer[0]);
|
||||||
|
}
|
||||||
+185
-179
@@ -10,10 +10,11 @@ import {
|
|||||||
committedTrackerData,
|
committedTrackerData,
|
||||||
$infoBoxContainer
|
$infoBoxContainer
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveChatData } from '../../core/persistence.js';
|
import { saveChatData, setMessageSwipeTrackerField } from '../../core/persistence.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { isItemLocked } from '../generation/lockManager.js';
|
import { isItemLocked } from '../generation/lockManager.js';
|
||||||
import { repairJSON } from '../../utils/jsonRepair.js';
|
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||||
|
import { updateFabWidgets } from '../ui/mobile.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to generate lock icon HTML if setting is enabled
|
* Helper to generate lock icon HTML if setting is enabled
|
||||||
@@ -158,171 +159,171 @@ export function renderInfoBox() {
|
|||||||
const lines = infoBoxData.split('\n');
|
const lines = infoBoxData.split('\n');
|
||||||
// console.log('[RPG Companion] Info Box split into lines:', lines);
|
// console.log('[RPG Companion] Info Box split into lines:', lines);
|
||||||
|
|
||||||
// Track which fields we've already parsed to avoid duplicates from mixed formats
|
// Track which fields we've already parsed to avoid duplicates from mixed formats
|
||||||
const parsedFields = {
|
const parsedFields = {
|
||||||
date: false,
|
date: false,
|
||||||
temperature: false,
|
temperature: false,
|
||||||
time: false,
|
time: false,
|
||||||
location: false,
|
location: false,
|
||||||
weather: false
|
weather: false
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
// console.log('[RPG Companion] Processing line:', line);
|
// console.log('[RPG Companion] Processing line:', line);
|
||||||
|
|
||||||
// Support both new text format (Date:) and legacy emoji format (🗓️:)
|
// Support both new text format (Date:) and legacy emoji format (🗓️:)
|
||||||
// Prioritize text format over emoji format
|
// Prioritize text format over emoji format
|
||||||
if (line.startsWith('Date:')) {
|
if (line.startsWith('Date:')) {
|
||||||
if (!parsedFields.date) {
|
if (!parsedFields.date) {
|
||||||
// console.log('[RPG Companion] → Matched DATE (text format)');
|
// console.log('[RPG Companion] → Matched DATE (text format)');
|
||||||
const dateStr = line.replace('Date:', '').trim();
|
const dateStr = line.replace('Date:', '').trim();
|
||||||
const dateParts = dateStr.split(',').map(p => p.trim());
|
const dateParts = dateStr.split(',').map(p => p.trim());
|
||||||
data.weekday = dateParts[0] || '';
|
data.weekday = dateParts[0] || '';
|
||||||
data.month = dateParts[1] || '';
|
data.month = dateParts[1] || '';
|
||||||
data.year = dateParts[2] || '';
|
data.year = dateParts[2] || '';
|
||||||
data.date = dateStr;
|
data.date = dateStr;
|
||||||
parsedFields.date = true;
|
parsedFields.date = true;
|
||||||
}
|
|
||||||
} else if (line.includes('🗓️:')) {
|
|
||||||
if (!parsedFields.date) {
|
|
||||||
// console.log('[RPG Companion] → Matched DATE (emoji format)');
|
|
||||||
const dateStr = line.replace('🗓️:', '').trim();
|
|
||||||
const dateParts = dateStr.split(',').map(p => p.trim());
|
|
||||||
data.weekday = dateParts[0] || '';
|
|
||||||
data.month = dateParts[1] || '';
|
|
||||||
data.year = dateParts[2] || '';
|
|
||||||
data.date = dateStr;
|
|
||||||
parsedFields.date = true;
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('Temperature:')) {
|
|
||||||
if (!parsedFields.temperature) {
|
|
||||||
// console.log('[RPG Companion] → Matched TEMPERATURE (text format)');
|
|
||||||
const tempStr = line.replace('Temperature:', '').trim();
|
|
||||||
data.temperature = tempStr;
|
|
||||||
const tempMatch = tempStr.match(/(-?\d+)/);
|
|
||||||
if (tempMatch) {
|
|
||||||
data.tempValue = parseInt(tempMatch[1]);
|
|
||||||
}
|
}
|
||||||
parsedFields.temperature = true;
|
} else if (line.includes('🗓️:')) {
|
||||||
}
|
if (!parsedFields.date) {
|
||||||
} else if (line.includes('🌡️:')) {
|
// console.log('[RPG Companion] → Matched DATE (emoji format)');
|
||||||
if (!parsedFields.temperature) {
|
const dateStr = line.replace('🗓️:', '').trim();
|
||||||
// console.log('[RPG Companion] → Matched TEMPERATURE (emoji format)');
|
const dateParts = dateStr.split(',').map(p => p.trim());
|
||||||
const tempStr = line.replace('🌡️:', '').trim();
|
data.weekday = dateParts[0] || '';
|
||||||
data.temperature = tempStr;
|
data.month = dateParts[1] || '';
|
||||||
const tempMatch = tempStr.match(/(-?\d+)/);
|
data.year = dateParts[2] || '';
|
||||||
if (tempMatch) {
|
data.date = dateStr;
|
||||||
data.tempValue = parseInt(tempMatch[1]);
|
parsedFields.date = true;
|
||||||
}
|
}
|
||||||
parsedFields.temperature = true;
|
} else if (line.startsWith('Temperature:')) {
|
||||||
}
|
if (!parsedFields.temperature) {
|
||||||
} else if (line.startsWith('Time:')) {
|
// console.log('[RPG Companion] → Matched TEMPERATURE (text format)');
|
||||||
if (!parsedFields.time) {
|
const tempStr = line.replace('Temperature:', '').trim();
|
||||||
// console.log('[RPG Companion] → Matched TIME (text format)');
|
data.temperature = tempStr;
|
||||||
const timeStr = line.replace('Time:', '').trim();
|
const tempMatch = tempStr.match(/(-?\d+)/);
|
||||||
data.time = timeStr;
|
if (tempMatch) {
|
||||||
const timeParts = timeStr.split('→').map(t => t.trim());
|
data.tempValue = parseInt(tempMatch[1]);
|
||||||
data.timeStart = timeParts[0] || '';
|
}
|
||||||
data.timeEnd = timeParts[1] || '';
|
parsedFields.temperature = true;
|
||||||
parsedFields.time = true;
|
|
||||||
}
|
|
||||||
} else if (line.includes('🕒:')) {
|
|
||||||
if (!parsedFields.time) {
|
|
||||||
// console.log('[RPG Companion] → Matched TIME (emoji format)');
|
|
||||||
const timeStr = line.replace('🕒:', '').trim();
|
|
||||||
data.time = timeStr;
|
|
||||||
const timeParts = timeStr.split('→').map(t => t.trim());
|
|
||||||
data.timeStart = timeParts[0] || '';
|
|
||||||
data.timeEnd = timeParts[1] || '';
|
|
||||||
parsedFields.time = true;
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('Location:')) {
|
|
||||||
if (!parsedFields.location) {
|
|
||||||
// console.log('[RPG Companion] → Matched LOCATION (text format)');
|
|
||||||
data.location = line.replace('Location:', '').trim();
|
|
||||||
parsedFields.location = true;
|
|
||||||
}
|
|
||||||
} else if (line.includes('🗺️:')) {
|
|
||||||
if (!parsedFields.location) {
|
|
||||||
// console.log('[RPG Companion] → Matched LOCATION (emoji format)');
|
|
||||||
data.location = line.replace('🗺️:', '').trim();
|
|
||||||
parsedFields.location = true;
|
|
||||||
}
|
|
||||||
} else if (line.startsWith('Weather:')) {
|
|
||||||
if (!parsedFields.weather) {
|
|
||||||
// New text format: Weather: [Emoji], [Forecast] OR Weather: [Emoji][Forecast] (no separator - FIXED)
|
|
||||||
const weatherStr = line.replace('Weather:', '').trim();
|
|
||||||
const { emoji, text } = separateEmojiFromText(weatherStr);
|
|
||||||
|
|
||||||
if (emoji && text) {
|
|
||||||
data.weatherEmoji = emoji;
|
|
||||||
data.weatherForecast = text;
|
|
||||||
} else if (weatherStr.includes(',')) {
|
|
||||||
// Fallback to comma split if emoji detection failed - split only on FIRST comma
|
|
||||||
const firstCommaIndex = weatherStr.indexOf(',');
|
|
||||||
data.weatherEmoji = weatherStr.substring(0, firstCommaIndex).trim();
|
|
||||||
data.weatherForecast = weatherStr.substring(firstCommaIndex + 1).trim();
|
|
||||||
} else {
|
|
||||||
// No clear separation - assume it's all forecast text
|
|
||||||
data.weatherEmoji = '🌤️'; // Default emoji
|
|
||||||
data.weatherForecast = weatherStr;
|
|
||||||
}
|
}
|
||||||
|
} else if (line.includes('🌡️:')) {
|
||||||
|
if (!parsedFields.temperature) {
|
||||||
|
// console.log('[RPG Companion] → Matched TEMPERATURE (emoji format)');
|
||||||
|
const tempStr = line.replace('🌡️:', '').trim();
|
||||||
|
data.temperature = tempStr;
|
||||||
|
const tempMatch = tempStr.match(/(-?\d+)/);
|
||||||
|
if (tempMatch) {
|
||||||
|
data.tempValue = parseInt(tempMatch[1]);
|
||||||
|
}
|
||||||
|
parsedFields.temperature = true;
|
||||||
|
}
|
||||||
|
} else if (line.startsWith('Time:')) {
|
||||||
|
if (!parsedFields.time) {
|
||||||
|
// console.log('[RPG Companion] → Matched TIME (text format)');
|
||||||
|
const timeStr = line.replace('Time:', '').trim();
|
||||||
|
data.time = timeStr;
|
||||||
|
const timeParts = timeStr.split('→').map(t => t.trim());
|
||||||
|
data.timeStart = timeParts[0] || '';
|
||||||
|
data.timeEnd = timeParts[1] || '';
|
||||||
|
parsedFields.time = true;
|
||||||
|
}
|
||||||
|
} else if (line.includes('🕒:')) {
|
||||||
|
if (!parsedFields.time) {
|
||||||
|
// console.log('[RPG Companion] → Matched TIME (emoji format)');
|
||||||
|
const timeStr = line.replace('🕒:', '').trim();
|
||||||
|
data.time = timeStr;
|
||||||
|
const timeParts = timeStr.split('→').map(t => t.trim());
|
||||||
|
data.timeStart = timeParts[0] || '';
|
||||||
|
data.timeEnd = timeParts[1] || '';
|
||||||
|
parsedFields.time = true;
|
||||||
|
}
|
||||||
|
} else if (line.startsWith('Location:')) {
|
||||||
|
if (!parsedFields.location) {
|
||||||
|
// console.log('[RPG Companion] → Matched LOCATION (text format)');
|
||||||
|
data.location = line.replace('Location:', '').trim();
|
||||||
|
parsedFields.location = true;
|
||||||
|
}
|
||||||
|
} else if (line.includes('🗺️:')) {
|
||||||
|
if (!parsedFields.location) {
|
||||||
|
// console.log('[RPG Companion] → Matched LOCATION (emoji format)');
|
||||||
|
data.location = line.replace('🗺️:', '').trim();
|
||||||
|
parsedFields.location = true;
|
||||||
|
}
|
||||||
|
} else if (line.startsWith('Weather:')) {
|
||||||
|
if (!parsedFields.weather) {
|
||||||
|
// New text format: Weather: [Emoji], [Forecast] OR Weather: [Emoji][Forecast] (no separator - FIXED)
|
||||||
|
const weatherStr = line.replace('Weather:', '').trim();
|
||||||
|
const { emoji, text } = separateEmojiFromText(weatherStr);
|
||||||
|
|
||||||
parsedFields.weather = true;
|
if (emoji && text) {
|
||||||
}
|
data.weatherEmoji = emoji;
|
||||||
} else {
|
data.weatherForecast = text;
|
||||||
// Check if it's a legacy weather line (emoji format)
|
} else if (weatherStr.includes(',')) {
|
||||||
// Only parse if we haven't already found weather in text format
|
// Fallback to comma split if emoji detection failed - split only on FIRST comma
|
||||||
if (!parsedFields.weather) {
|
const firstCommaIndex = weatherStr.indexOf(',');
|
||||||
// Since \p{Emoji} doesn't work reliably, use a simpler approach
|
data.weatherEmoji = weatherStr.substring(0, firstCommaIndex).trim();
|
||||||
const hasColon = line.includes(':');
|
data.weatherForecast = weatherStr.substring(firstCommaIndex + 1).trim();
|
||||||
const notInfoBox = !line.includes('Info Box');
|
} else {
|
||||||
const notDivider = !line.includes('---');
|
// No clear separation - assume it's all forecast text
|
||||||
const notCodeFence = !line.trim().startsWith('```');
|
data.weatherEmoji = '🌤️'; // Default emoji
|
||||||
|
data.weatherForecast = weatherStr;
|
||||||
|
}
|
||||||
|
|
||||||
// console.log('[RPG Companion] → Checking weather conditions:', {
|
parsedFields.weather = true;
|
||||||
// line: line,
|
}
|
||||||
// hasColon: hasColon,
|
} else {
|
||||||
// notInfoBox: notInfoBox,
|
// Check if it's a legacy weather line (emoji format)
|
||||||
// notDivider: notDivider
|
// Only parse if we haven't already found weather in text format
|
||||||
// });
|
if (!parsedFields.weather) {
|
||||||
|
// Since \p{Emoji} doesn't work reliably, use a simpler approach
|
||||||
|
const hasColon = line.includes(':');
|
||||||
|
const notInfoBox = !line.includes('Info Box');
|
||||||
|
const notDivider = !line.includes('---');
|
||||||
|
const notCodeFence = !line.trim().startsWith('```');
|
||||||
|
|
||||||
if (hasColon && notInfoBox && notDivider && notCodeFence && line.trim().length > 0) {
|
// console.log('[RPG Companion] → Checking weather conditions:', {
|
||||||
// Match format: [Weather Emoji]: [Forecast]
|
// line: line,
|
||||||
// Capture everything before colon as emoji, everything after as forecast
|
// hasColon: hasColon,
|
||||||
// console.log('[RPG Companion] → Testing WEATHER match for:', line);
|
// notInfoBox: notInfoBox,
|
||||||
const weatherMatch = line.match(/^\s*([^:]+):\s*(.+)$/);
|
// notDivider: notDivider
|
||||||
if (weatherMatch) {
|
// });
|
||||||
const potentialEmoji = weatherMatch[1].trim();
|
|
||||||
const forecast = weatherMatch[2].trim();
|
|
||||||
|
|
||||||
// If the first part is short (likely emoji), treat as weather
|
if (hasColon && notInfoBox && notDivider && notCodeFence && line.trim().length > 0) {
|
||||||
if (potentialEmoji.length <= 5) {
|
// Match format: [Weather Emoji]: [Forecast]
|
||||||
data.weatherEmoji = potentialEmoji;
|
// Capture everything before colon as emoji, everything after as forecast
|
||||||
data.weatherForecast = forecast;
|
// console.log('[RPG Companion] → Testing WEATHER match for:', line);
|
||||||
parsedFields.weather = true;
|
const weatherMatch = line.match(/^\s*([^:]+):\s*(.+)$/);
|
||||||
// console.log('[RPG Companion] ✓ Weather parsed:', data.weatherEmoji, data.weatherForecast);
|
if (weatherMatch) {
|
||||||
|
const potentialEmoji = weatherMatch[1].trim();
|
||||||
|
const forecast = weatherMatch[2].trim();
|
||||||
|
|
||||||
|
// If the first part is short (likely emoji), treat as weather
|
||||||
|
if (potentialEmoji.length <= 5) {
|
||||||
|
data.weatherEmoji = potentialEmoji;
|
||||||
|
data.weatherForecast = forecast;
|
||||||
|
parsedFields.weather = true;
|
||||||
|
// console.log('[RPG Companion] ✓ Weather parsed:', data.weatherEmoji, data.weatherForecast);
|
||||||
|
} else {
|
||||||
|
// console.log('[RPG Companion] ✗ First part too long for emoji:', potentialEmoji);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// console.log('[RPG Companion] ✗ First part too long for emoji:', potentialEmoji);
|
// console.log('[RPG Companion] ✗ Weather regex did not match');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// console.log('[RPG Companion] ✗ Weather regex did not match');
|
// console.log('[RPG Companion] → No match for this line');
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// console.log('[RPG Companion] → No match for this line');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('[RPG Companion] Parsed Info Box data:', {
|
// console.log('[RPG Companion] Parsed Info Box data:', {
|
||||||
// date: data.date,
|
// date: data.date,
|
||||||
// weatherEmoji: data.weatherEmoji,
|
// weatherEmoji: data.weatherEmoji,
|
||||||
// weatherForecast: data.weatherForecast,
|
// weatherForecast: data.weatherForecast,
|
||||||
// temperature: data.temperature,
|
// temperature: data.temperature,
|
||||||
// timeStart: data.timeStart,
|
// timeStart: data.timeStart,
|
||||||
// location: data.location
|
// location: data.location
|
||||||
// });
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get tracker configuration
|
// Get tracker configuration
|
||||||
@@ -362,9 +363,9 @@ export function renderInfoBox() {
|
|||||||
row1Widgets.push(`
|
row1Widgets.push(`
|
||||||
<div class="rpg-dashboard-widget rpg-calendar-widget">
|
<div class="rpg-dashboard-widget rpg-calendar-widget">
|
||||||
${dateLockIconHtml}
|
${dateLockIconHtml}
|
||||||
<div class="rpg-calendar-top rpg-editable" contenteditable="true" data-field="month" data-full-value="${data.month || ''}" title="Click to edit">${monthDisplay}</div>
|
<div class="rpg-calendar-top rpg-editable" contenteditable="true" data-field="month" data-full-value="${data.month || ''}" title="${i18n.getTranslation('infoBox.clickToEdit') || 'Click to edit'}">${monthDisplay}</div>
|
||||||
<div class="rpg-calendar-day" title="Click to edit"><span class="rpg-calendar-day-text rpg-editable" contenteditable="true" data-field="weekday" data-full-value="${data.weekday || ''}">${weekdayDisplay}</span></div>
|
<div class="rpg-calendar-day" title="${i18n.getTranslation('infoBox.clickToEdit') || 'Click to edit'}"><span class="rpg-calendar-day-text rpg-editable" contenteditable="true" data-field="weekday" data-full-value="${data.weekday || ''}">${weekdayDisplay}</span></div>
|
||||||
<div class="rpg-calendar-year rpg-editable" contenteditable="true" data-field="year" data-full-value="${data.year || ''}" title="Click to edit">${yearDisplay}</div>
|
<div class="rpg-calendar-year rpg-editable" contenteditable="true" data-field="year" data-full-value="${data.year || ''}" title="${i18n.getTranslation('infoBox.clickToEdit') || 'Click to edit'}">${yearDisplay}</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@@ -372,14 +373,14 @@ export function renderInfoBox() {
|
|||||||
// Weather widget - show if enabled
|
// Weather widget - show if enabled
|
||||||
if (config?.widgets?.weather?.enabled) {
|
if (config?.widgets?.weather?.enabled) {
|
||||||
const weatherEmoji = data.weatherEmoji || '🌤️';
|
const weatherEmoji = data.weatherEmoji || '🌤️';
|
||||||
const weatherForecast = data.weatherForecast || 'Weather';
|
const weatherForecast = data.weatherForecast || i18n.getTranslation('infoBox.weatherFallback') || 'Weather unknown';
|
||||||
const weatherLockIconHtml = getLockIconHtml('infoBox', 'weather');
|
const weatherLockIconHtml = getLockIconHtml('infoBox', 'weather');
|
||||||
|
|
||||||
row1Widgets.push(`
|
row1Widgets.push(`
|
||||||
<div class="rpg-dashboard-widget rpg-weather-widget">
|
<div class="rpg-dashboard-widget rpg-weather-widget">
|
||||||
${weatherLockIconHtml}
|
${weatherLockIconHtml}
|
||||||
<div class="rpg-weather-icon rpg-editable" contenteditable="true" data-field="weatherEmoji" title="Click to edit emoji">${weatherEmoji}</div>
|
<div class="rpg-weather-icon rpg-editable" contenteditable="true" data-field="weatherEmoji" title="${i18n.getTranslation('userStats.clickToEditEmoji') || 'Click to edit emoji'}">${weatherEmoji}</div>
|
||||||
<div class="rpg-weather-forecast rpg-editable" contenteditable="true" data-field="weatherForecast" title="Click to edit">${weatherForecast}</div>
|
<div class="rpg-weather-forecast rpg-editable" contenteditable="true" data-field="weatherForecast" title="${i18n.getTranslation('infoBox.clickToEdit') || 'Click to edit'}">${weatherForecast}</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@@ -398,12 +399,12 @@ export function renderInfoBox() {
|
|||||||
|
|
||||||
if (preferredUnit === 'F' && isCelsius) {
|
if (preferredUnit === 'F' && isCelsius) {
|
||||||
// Convert C to F
|
// Convert C to F
|
||||||
const fahrenheit = Math.round((tempValue * 9/5) + 32);
|
const fahrenheit = Math.round((tempValue * 9 / 5) + 32);
|
||||||
tempDisplay = `${fahrenheit}°F`;
|
tempDisplay = `${fahrenheit}°F`;
|
||||||
tempValue = fahrenheit;
|
tempValue = fahrenheit;
|
||||||
} else if (preferredUnit === 'C' && isFahrenheit) {
|
} else if (preferredUnit === 'C' && isFahrenheit) {
|
||||||
// Convert F to C
|
// Convert F to C
|
||||||
const celsius = Math.round((tempValue - 32) * 5/9);
|
const celsius = Math.round((tempValue - 32) * 5 / 9);
|
||||||
tempDisplay = `${celsius}°C`;
|
tempDisplay = `${celsius}°C`;
|
||||||
tempValue = celsius;
|
tempValue = celsius;
|
||||||
}
|
}
|
||||||
@@ -414,7 +415,7 @@ export function renderInfoBox() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate thermometer display (convert to Celsius for consistent thresholds)
|
// Calculate thermometer display (convert to Celsius for consistent thresholds)
|
||||||
const tempInCelsius = preferredUnit === 'F' ? Math.round((tempValue - 32) * 5/9) : tempValue;
|
const tempInCelsius = preferredUnit === 'F' ? Math.round((tempValue - 32) * 5 / 9) : tempValue;
|
||||||
const tempPercent = Math.min(100, Math.max(0, ((tempInCelsius + 20) / 60) * 100));
|
const tempPercent = Math.min(100, Math.max(0, ((tempInCelsius + 20) / 60) * 100));
|
||||||
const tempColor = tempInCelsius < 10 ? '#4a90e2' : tempInCelsius < 25 ? '#67c23a' : '#e94560';
|
const tempColor = tempInCelsius < 10 ? '#4a90e2' : tempInCelsius < 25 ? '#67c23a' : '#e94560';
|
||||||
const tempLockIconHtml = getLockIconHtml('infoBox', 'temperature');
|
const tempLockIconHtml = getLockIconHtml('infoBox', 'temperature');
|
||||||
@@ -428,21 +429,19 @@ export function renderInfoBox() {
|
|||||||
<div class="rpg-thermometer-fill" style="height: ${tempPercent}%; background: ${tempColor}"></div>
|
<div class="rpg-thermometer-fill" style="height: ${tempPercent}%; background: ${tempColor}"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-temp-value rpg-editable" contenteditable="true" data-field="temperature" title="Click to edit">${tempDisplay}</div>
|
<div class="rpg-temp-value rpg-editable" contenteditable="true" data-field="temperature" title="${i18n.getTranslation('infoBox.clickToEdit') || 'Click to edit'}">${tempDisplay}</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time widget - show if enabled
|
// Time widget - show if enabled
|
||||||
if (config?.widgets?.time?.enabled) {
|
if (config?.widgets?.time?.enabled) {
|
||||||
// Determine which time value to display and edit
|
// Get both start and end times
|
||||||
const hasTimeEnd = Boolean(data.timeEnd);
|
const timeStartDisplay = data.timeStart || '12:00';
|
||||||
const hasTimeStart = Boolean(data.timeStart);
|
const timeEndDisplay = data.timeEnd || data.timeStart || '12:00';
|
||||||
const timeDisplay = data.timeEnd || data.timeStart || '12:00';
|
|
||||||
const timeField = hasTimeEnd ? 'timeEnd' : 'timeStart';
|
|
||||||
|
|
||||||
// Parse time for clock hands
|
// Parse end time for clock hands (use end time for visual display)
|
||||||
const timeMatch = timeDisplay.match(/(\d+):(\d+)/);
|
const timeMatch = timeEndDisplay.match(/(\d+):(\d+)/);
|
||||||
let hourAngle = 0;
|
let hourAngle = 0;
|
||||||
let minuteAngle = 0;
|
let minuteAngle = 0;
|
||||||
if (timeMatch) {
|
if (timeMatch) {
|
||||||
@@ -464,7 +463,11 @@ export function renderInfoBox() {
|
|||||||
<div class="rpg-clock-center"></div>
|
<div class="rpg-clock-center"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="${timeField}" title="Click to edit">${timeDisplay}</div>
|
<div class="rpg-time-range">
|
||||||
|
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="timeStart" title="${i18n.getTranslation('infoBox.clickToEdit') || 'Click to edit'}">${timeStartDisplay}</div>
|
||||||
|
<span class="rpg-time-separator">→</span>
|
||||||
|
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="timeEnd" title="${i18n.getTranslation('infoBox.clickToEdit') || 'Click to edit'}">${timeEndDisplay}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@@ -478,7 +481,7 @@ export function renderInfoBox() {
|
|||||||
|
|
||||||
// Row 2: Location widget (full width) - show if enabled
|
// Row 2: Location widget (full width) - show if enabled
|
||||||
if (config?.widgets?.location?.enabled) {
|
if (config?.widgets?.location?.enabled) {
|
||||||
const locationDisplay = data.location || 'Location';
|
const locationDisplay = data.location || i18n.getTranslation('infoBox.locationFallback') || 'Unknown location';
|
||||||
const locationLockIconHtml = getLockIconHtml('infoBox', 'location');
|
const locationLockIconHtml = getLockIconHtml('infoBox', 'location');
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
@@ -488,7 +491,7 @@ export function renderInfoBox() {
|
|||||||
<div class="rpg-map-bg">
|
<div class="rpg-map-bg">
|
||||||
<div class="rpg-map-marker">📍</div>
|
<div class="rpg-map-marker">📍</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-location-text rpg-editable" contenteditable="true" data-field="location" title="Click to edit">${locationDisplay}</div>
|
<div class="rpg-location-text rpg-editable" contenteditable="true" data-field="location" title="${i18n.getTranslation('infoBox.clickToEdit') || 'Click to edit'}">${locationDisplay}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -538,7 +541,7 @@ export function renderInfoBox() {
|
|||||||
<div class="rpg-notebook-ring"></div>
|
<div class="rpg-notebook-ring"></div>
|
||||||
<div class="rpg-notebook-ring"></div>
|
<div class="rpg-notebook-ring"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-notebook-title" data-i18n-key="infobox.recentEvents.title">${i18n.getTranslation('infobox.recentEvents.title')}</div>
|
<div class="rpg-notebook-title" data-i18n-key="infobox.recentEvents.title">${i18n.getTranslation('infobox.recentEvents.title') || 'Recent Events'}</div>
|
||||||
<div class="rpg-notebook-lines">
|
<div class="rpg-notebook-lines">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -547,7 +550,7 @@ export function renderInfoBox() {
|
|||||||
html += `
|
html += `
|
||||||
<div class="rpg-notebook-line">
|
<div class="rpg-notebook-line">
|
||||||
<span class="rpg-bullet">•</span>
|
<span class="rpg-bullet">•</span>
|
||||||
<span class="rpg-event-text rpg-editable" contenteditable="true" data-field="event${i + 1}" title="Click to edit">${validEvents[i]}</span>
|
<span class="rpg-event-text rpg-editable" contenteditable="true" data-field="event${i + 1}" title="${i18n.getTranslation('infoBox.clickToEdit') || 'Click to edit'}">${validEvents[i]}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -557,7 +560,7 @@ export function renderInfoBox() {
|
|||||||
html += `
|
html += `
|
||||||
<div class="rpg-notebook-line rpg-event-add">
|
<div class="rpg-notebook-line rpg-event-add">
|
||||||
<span class="rpg-bullet">+</span>
|
<span class="rpg-bullet">+</span>
|
||||||
<span class="rpg-event-text rpg-editable rpg-event-placeholder" contenteditable="true" data-field="event${i + 1}" title="Click to add event" data-i18n-key="infobox.recentEvents.addEventPlaceholder">${i18n.getTranslation('infobox.recentEvents.addEventPlaceholder')}</span>
|
<span class="rpg-event-text rpg-editable rpg-event-placeholder" contenteditable="true" data-field="event${i + 1}" title="Click to add event" data-i18n-key="infobox.recentEvents.addEventPlaceholder">${i18n.getTranslation('infobox.recentEvents.addEventPlaceholder') || 'Click to add event'}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -588,7 +591,7 @@ export function renderInfoBox() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add event handlers for editable Info Box fields
|
// Add event handlers for editable Info Box fields
|
||||||
$infoBoxContainer.find('.rpg-editable').on('blur', function() {
|
$infoBoxContainer.find('.rpg-editable').on('blur', function () {
|
||||||
const $this = $(this);
|
const $this = $(this);
|
||||||
const field = $this.data('field');
|
const field = $this.data('field');
|
||||||
const value = $this.text().trim();
|
const value = $this.text().trim();
|
||||||
@@ -615,15 +618,18 @@ export function renderInfoBox() {
|
|||||||
} else {
|
} else {
|
||||||
updateInfoBoxField(field, value);
|
updateInfoBoxField(field, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update FAB widgets to reflect changes
|
||||||
|
updateFabWidgets();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update location size on input as well (real-time)
|
// Update location size on input as well (real-time)
|
||||||
$infoBoxContainer.find('[data-field="location"]').on('input', function() {
|
$infoBoxContainer.find('[data-field="location"]').on('input', function () {
|
||||||
updateLocationTextSize($(this));
|
updateLocationTextSize($(this));
|
||||||
});
|
});
|
||||||
|
|
||||||
// For date fields, show full value on focus
|
// For date fields, show full value on focus
|
||||||
$infoBoxContainer.find('[data-field="month"], [data-field="weekday"], [data-field="year"]').on('focus', function() {
|
$infoBoxContainer.find('[data-field="month"], [data-field="weekday"], [data-field="year"]').on('focus', function () {
|
||||||
const fullValue = $(this).data('full-value');
|
const fullValue = $(this).data('full-value');
|
||||||
if (fullValue) {
|
if (fullValue) {
|
||||||
$(this).text(fullValue);
|
$(this).text(fullValue);
|
||||||
@@ -631,7 +637,7 @@ export function renderInfoBox() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add event handler for lock icons (support both click and touch)
|
// Add event handler for lock icons (support both click and touch)
|
||||||
$infoBoxContainer.find('.rpg-section-lock-icon').on('click touchend', function(e) {
|
$infoBoxContainer.find('.rpg-section-lock-icon').on('click touchend', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const $lockIcon = $(this);
|
const $lockIcon = $(this);
|
||||||
@@ -646,7 +652,7 @@ export function renderInfoBox() {
|
|||||||
|
|
||||||
// Update icon
|
// Update icon
|
||||||
$lockIcon.text(newLockState ? '🔒' : '🔓');
|
$lockIcon.text(newLockState ? '🔒' : '🔓');
|
||||||
$lockIcon.attr('title', newLockState ? 'Locked - AI cannot change this' : 'Unlocked - AI can change this');
|
$lockIcon.attr('title', newLockState ? (i18n.getTranslation('infoBox.locked') || 'Locked') : (i18n.getTranslation('infoBox.unlocked') || 'Unlocked'));
|
||||||
$lockIcon.toggleClass('locked', newLockState);
|
$lockIcon.toggleClass('locked', newLockState);
|
||||||
|
|
||||||
// Save settings to persist lock state
|
// Save settings to persist lock state
|
||||||
@@ -983,7 +989,7 @@ export function updateInfoBoxField(field, value) {
|
|||||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
if (message.extra && message.extra.rpg_companion_swipes) {
|
||||||
const swipeId = message.swipe_id || 0;
|
const swipeId = message.swipe_id || 0;
|
||||||
if (message.extra.rpg_companion_swipes[swipeId]) {
|
if (message.extra.rpg_companion_swipes[swipeId]) {
|
||||||
message.extra.rpg_companion_swipes[swipeId].infoBox = updatedLines.join('\n');
|
setMessageSwipeTrackerField(message, swipeId, 'infoBox', updatedLines.join('\n'));
|
||||||
// console.log('[RPG Companion] Updated infoBox in message swipe data');
|
// console.log('[RPG Companion] Updated infoBox in message swipe data');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1068,7 +1074,7 @@ function updateRecentEvent(field, value) {
|
|||||||
if (message.extra && message.extra.rpg_companion_swipes) {
|
if (message.extra && message.extra.rpg_companion_swipes) {
|
||||||
const swipeId = message.swipe_id || 0;
|
const swipeId = message.swipe_id || 0;
|
||||||
if (message.extra.rpg_companion_swipes[swipeId]) {
|
if (message.extra.rpg_companion_swipes[swipeId]) {
|
||||||
message.extra.rpg_companion_swipes[swipeId].infoBox = updatedLines.join('\n');
|
setMessageSwipeTrackerField(message, swipeId, 'infoBox', updatedLines.join('\n'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { getInventoryRenderOptions, restoreFormStates } from '../interaction/inv
|
|||||||
import { updateInventoryItem } from '../interaction/inventoryEdit.js';
|
import { updateInventoryItem } from '../interaction/inventoryEdit.js';
|
||||||
import { parseItems } from '../../utils/itemParser.js';
|
import { parseItems } from '../../utils/itemParser.js';
|
||||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
// Type imports
|
// Type imports
|
||||||
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
|
||||||
@@ -25,7 +26,7 @@ function getLockIconHtml(tracker, path) {
|
|||||||
|
|
||||||
const isLocked = isItemLocked(tracker, path);
|
const isLocked = isItemLocked(tracker, path);
|
||||||
const lockIcon = isLocked ? '🔒' : '🔓';
|
const lockIcon = isLocked ? '🔒' : '🔓';
|
||||||
const lockTitle = isLocked ? 'Locked' : 'Unlocked';
|
const lockTitle = isLocked ? i18n.getTranslation('global.locked') || 'Locked' : i18n.getTranslation('global.unlocked') || 'Unlocked';
|
||||||
const lockedClass = isLocked ? ' locked' : '';
|
const lockedClass = isLocked ? ' locked' : '';
|
||||||
return `<span class="rpg-section-lock-icon${lockedClass}" data-tracker="${tracker}" data-path="${path}" title="${lockTitle}">${lockIcon}</span>`;
|
return `<span class="rpg-section-lock-icon${lockedClass}" data-tracker="${tracker}" data-path="${path}" title="${lockTitle}">${lockIcon}</span>`;
|
||||||
}
|
}
|
||||||
@@ -47,19 +48,24 @@ export function getLocationId(locationName) {
|
|||||||
* @returns {string} HTML for sub-tab navigation
|
* @returns {string} HTML for sub-tab navigation
|
||||||
*/
|
*/
|
||||||
export function renderInventorySubTabs(activeTab = 'onPerson') {
|
export function renderInventorySubTabs(activeTab = 'onPerson') {
|
||||||
|
const onPersonText = i18n.getTranslation('inventory.section.onPerson') || 'On Person';
|
||||||
|
const clothingText = i18n.getTranslation('inventory.section.clothing') || 'Clothing';
|
||||||
|
const storedText = i18n.getTranslation('inventory.section.stored') || 'Stored';
|
||||||
|
const assetsText = i18n.getTranslation('inventory.section.assets') || 'Assets';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="rpg-inventory-subtabs">
|
<div class="rpg-inventory-subtabs">
|
||||||
<button class="rpg-inventory-subtab ${activeTab === 'onPerson' ? 'active' : ''}" data-tab="onPerson">
|
<button class="rpg-inventory-subtab ${activeTab === 'onPerson' ? 'active' : ''}" data-tab="onPerson">
|
||||||
On Person
|
${onPersonText}
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-inventory-subtab ${activeTab === 'clothing' ? 'active' : ''}" data-tab="clothing">
|
<button class="rpg-inventory-subtab ${activeTab === 'clothing' ? 'active' : ''}" data-tab="clothing">
|
||||||
Clothing
|
${clothingText}
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-inventory-subtab ${activeTab === 'stored' ? 'active' : ''}" data-tab="stored">
|
<button class="rpg-inventory-subtab ${activeTab === 'stored' ? 'active' : ''}" data-tab="stored">
|
||||||
Stored
|
${storedText}
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-inventory-subtab ${activeTab === 'assets' ? 'active' : ''}" data-tab="assets">
|
<button class="rpg-inventory-subtab ${activeTab === 'assets' ? 'active' : ''}" data-tab="assets">
|
||||||
Assets
|
${assetsText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -76,30 +82,30 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
|
|||||||
|
|
||||||
let itemsHtml = '';
|
let itemsHtml = '';
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
itemsHtml = '<div class="rpg-inventory-empty">No items carried</div>';
|
itemsHtml = '<div class="rpg-inventory-empty">' + (i18n.getTranslation('inventory.onPerson.empty') || 'No items carried') + '</div>';
|
||||||
} else {
|
} else {
|
||||||
if (viewMode === 'grid') {
|
if (viewMode === 'grid') {
|
||||||
// Grid view: card-style items
|
// Grid view: card-style items
|
||||||
itemsHtml = items.map((item, index) => {
|
itemsHtml = items.map((item, index) => {
|
||||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson[${index}]`);
|
const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson.${item}`);
|
||||||
return `
|
return `
|
||||||
<div class="rpg-item-card" data-field="onPerson" data-index="${index}">
|
<div class="rpg-item-card" data-field="onPerson" data-index="${index}">
|
||||||
${lockIconHtml}
|
${lockIconHtml}
|
||||||
<button class="rpg-item-remove" data-action="remove-item" data-field="onPerson" data-index="${index}" title="Remove item">
|
<button class="rpg-item-remove" data-action="remove-item" data-field="onPerson" data-index="${index}" title="${i18n.getTranslation('global.removeItem') || 'Remove item'}">
|
||||||
<i class="fa-solid fa-times"></i>
|
<i class="fa-solid fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="onPerson" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="onPerson" data-index="${index}" title="${i18n.getTranslation('global.clickToEdit') || 'Click to edit'}">${escapeHtml(item)}</span>
|
||||||
</div>
|
</div>
|
||||||
`}).join('');
|
`}).join('');
|
||||||
} else {
|
} else {
|
||||||
// List view: full-width rows
|
// List view: full-width rows
|
||||||
itemsHtml = items.map((item, index) => {
|
itemsHtml = items.map((item, index) => {
|
||||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson[${index}]`);
|
const lockIconHtml = getLockIconHtml('userStats', `inventory.onPerson.${item}`);
|
||||||
return `
|
return `
|
||||||
<div class="rpg-item-row" data-field="onPerson" data-index="${index}">
|
<div class="rpg-item-row" data-field="onPerson" data-index="${index}">
|
||||||
${lockIconHtml}
|
${lockIconHtml}
|
||||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="onPerson" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="onPerson" data-index="${index}" title="${i18n.getTranslation('global.clickToEdit') || 'Click to edit'}">${escapeHtml(item)}</span>
|
||||||
<button class="rpg-item-remove" data-action="remove-item" data-field="onPerson" data-index="${index}" title="Remove item">
|
<button class="rpg-item-remove" data-action="remove-item" data-field="onPerson" data-index="${index}" title="${i18n.getTranslation('global.removeItem') || 'Remove item'}">
|
||||||
<i class="fa-solid fa-times"></i>
|
<i class="fa-solid fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,30 +118,30 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
|
|||||||
return `
|
return `
|
||||||
<div class="rpg-inventory-section" data-section="onPerson">
|
<div class="rpg-inventory-section" data-section="onPerson">
|
||||||
<div class="rpg-inventory-header">
|
<div class="rpg-inventory-header">
|
||||||
<h4>Items Currently Carried</h4>
|
<h4>${i18n.getTranslation('inventory.onPerson.title') || 'Items Currently Carried'}</h4>
|
||||||
<div class="rpg-inventory-header-actions">
|
<div class="rpg-inventory-header-actions">
|
||||||
<div class="rpg-view-toggle">
|
<div class="rpg-view-toggle">
|
||||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="onPerson" data-view="list" title="List view">
|
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="onPerson" data-view="list" title="${i18n.getTranslation('global.listView') || 'List view'}">
|
||||||
<i class="fa-solid fa-list"></i>
|
<i class="fa-solid fa-list"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="onPerson" data-view="grid" title="Grid view">
|
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="onPerson" data-view="grid" title="${i18n.getTranslation('global.gridView') || 'Grid view'}">
|
||||||
<i class="fa-solid fa-th"></i>
|
<i class="fa-solid fa-th"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="onPerson" title="Add new item">
|
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="onPerson" title="${i18n.getTranslation('inventory.onPerson.addItemTitle') || 'Add new item'}">
|
||||||
<i class="fa-solid fa-plus"></i> Add Item
|
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('inventory.onPerson.addItemButton') || 'Add Item'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-inventory-content">
|
<div class="rpg-inventory-content">
|
||||||
<div class="rpg-inline-form" id="rpg-add-item-form-onPerson" style="display: none;">
|
<div class="rpg-inline-form" id="rpg-add-item-form-onPerson" style="display: none;">
|
||||||
<input type="text" class="rpg-inline-input" id="rpg-new-item-onPerson" placeholder="Enter item name..." />
|
<input type="text" class="rpg-inline-input" id="rpg-new-item-onPerson" placeholder="${i18n.getTranslation('inventory.onPerson.addItemPlaceholder') || 'Enter item name...'}" />
|
||||||
<div class="rpg-inline-buttons">
|
<div class="rpg-inline-buttons">
|
||||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="onPerson">
|
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="onPerson">
|
||||||
<i class="fa-solid fa-times"></i> Cancel
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="onPerson">
|
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="onPerson">
|
||||||
<i class="fa-solid fa-check"></i> Add
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.add') || 'Add'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,30 +164,30 @@ export function renderClothingView(clothingItems, viewMode = 'list') {
|
|||||||
|
|
||||||
let itemsHtml = '';
|
let itemsHtml = '';
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
itemsHtml = '<div class="rpg-inventory-empty">No clothing worn</div>';
|
itemsHtml = '<div class="rpg-inventory-empty">' + (i18n.getTranslation('inventory.clothing.empty') || 'No clothing worn') + '</div>';
|
||||||
} else {
|
} else {
|
||||||
if (viewMode === 'grid') {
|
if (viewMode === 'grid') {
|
||||||
// Grid view: card-style items
|
// Grid view: card-style items
|
||||||
itemsHtml = items.map((item, index) => {
|
itemsHtml = items.map((item, index) => {
|
||||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing[${index}]`);
|
const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing.${item}`);
|
||||||
return `
|
return `
|
||||||
<div class="rpg-item-card" data-field="clothing" data-index="${index}">
|
<div class="rpg-item-card" data-field="clothing" data-index="${index}">
|
||||||
${lockIconHtml}
|
${lockIconHtml}
|
||||||
<button class="rpg-item-remove" data-action="remove-item" data-field="clothing" data-index="${index}" title="Remove item">
|
<button class="rpg-item-remove" data-action="remove-item" data-field="clothing" data-index="${index}" title="${i18n.getTranslation('global.removeItem') || 'Remove item'}">
|
||||||
<i class="fa-solid fa-times"></i>
|
<i class="fa-solid fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="clothing" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="clothing" data-index="${index}" title="${i18n.getTranslation('global.clickToEdit') || 'Click to edit'}">${escapeHtml(item)}</span>
|
||||||
</div>
|
</div>
|
||||||
`}).join('');
|
`}).join('');
|
||||||
} else {
|
} else {
|
||||||
// List view: full-width rows
|
// List view: full-width rows
|
||||||
itemsHtml = items.map((item, index) => {
|
itemsHtml = items.map((item, index) => {
|
||||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing[${index}]`);
|
const lockIconHtml = getLockIconHtml('userStats', `inventory.clothing.${item}`);
|
||||||
return `
|
return `
|
||||||
<div class="rpg-item-row" data-field="clothing" data-index="${index}">
|
<div class="rpg-item-row" data-field="clothing" data-index="${index}">
|
||||||
${lockIconHtml}
|
${lockIconHtml}
|
||||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="clothing" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="clothing" data-index="${index}" title="${i18n.getTranslation('global.clickToEdit') || 'Click to edit'}">${escapeHtml(item)}</span>
|
||||||
<button class="rpg-item-remove" data-action="remove-item" data-field="clothing" data-index="${index}" title="Remove item">
|
<button class="rpg-item-remove" data-action="remove-item" data-field="clothing" data-index="${index}" title="${i18n.getTranslation('global.removeItem') || 'Remove item'}">
|
||||||
<i class="fa-solid fa-times"></i>
|
<i class="fa-solid fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,30 +200,30 @@ export function renderClothingView(clothingItems, viewMode = 'list') {
|
|||||||
return `
|
return `
|
||||||
<div class="rpg-inventory-section" data-section="clothing">
|
<div class="rpg-inventory-section" data-section="clothing">
|
||||||
<div class="rpg-inventory-header">
|
<div class="rpg-inventory-header">
|
||||||
<h4>Clothing Worn</h4>
|
<h4>${i18n.getTranslation('inventory.clothing.title') || 'Clothing & Armor'}</h4>
|
||||||
<div class="rpg-inventory-header-actions">
|
<div class="rpg-inventory-header-actions">
|
||||||
<div class="rpg-view-toggle">
|
<div class="rpg-view-toggle">
|
||||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="clothing" data-view="list" title="List view">
|
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="clothing" data-view="list" title="${i18n.getTranslation('global.listView') || 'List view'}">
|
||||||
<i class="fa-solid fa-list"></i>
|
<i class="fa-solid fa-list"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="clothing" data-view="grid" title="Grid view">
|
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="clothing" data-view="grid" title="${i18n.getTranslation('global.gridView') || 'Grid view'}">
|
||||||
<i class="fa-solid fa-th"></i>
|
<i class="fa-solid fa-th"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="clothing" title="Add new clothing item">
|
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="clothing" title="${i18n.getTranslation('inventory.clothing.addItemTitle') || 'Add new clothing item'}">
|
||||||
<i class="fa-solid fa-plus"></i> Add Clothing
|
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('inventory.clothing.addItemButton') || 'Add Clothing'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-inventory-content">
|
<div class="rpg-inventory-content">
|
||||||
<div class="rpg-inline-form" id="rpg-add-item-form-clothing" style="display: none;">
|
<div class="rpg-inline-form" id="rpg-add-item-form-clothing" style="display: none;">
|
||||||
<input type="text" class="rpg-inline-input" id="rpg-new-item-clothing" placeholder="Enter clothing item..." />
|
<input type="text" class="rpg-inline-input" id="rpg-new-item-clothing" placeholder="${i18n.getTranslation('inventory.clothing.addItemPlaceholder') || 'Enter clothing item...'}" />
|
||||||
<div class="rpg-inline-buttons">
|
<div class="rpg-inline-buttons">
|
||||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="clothing">
|
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="clothing">
|
||||||
<i class="fa-solid fa-times"></i> Cancel
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="clothing">
|
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="clothing">
|
||||||
<i class="fa-solid fa-check"></i> Add
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.add') || 'Add'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,30 +248,30 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
|||||||
let html = `
|
let html = `
|
||||||
<div class="rpg-inventory-section" data-section="stored">
|
<div class="rpg-inventory-section" data-section="stored">
|
||||||
<div class="rpg-inventory-header">
|
<div class="rpg-inventory-header">
|
||||||
<h4>Storage Locations</h4>
|
<h4>${i18n.getTranslation('inventory.stored.title') || 'Storage Locations'}</h4>
|
||||||
<div class="rpg-inventory-header-actions">
|
<div class="rpg-inventory-header-actions">
|
||||||
<div class="rpg-view-toggle">
|
<div class="rpg-view-toggle">
|
||||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="stored" data-view="list" title="List view">
|
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="stored" data-view="list" title="${i18n.getTranslation('global.listView') || 'List view'}">
|
||||||
<i class="fa-solid fa-list"></i>
|
<i class="fa-solid fa-list"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="stored" data-view="grid" title="Grid view">
|
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="stored" data-view="grid" title="${i18n.getTranslation('global.gridView') || 'Grid view'}">
|
||||||
<i class="fa-solid fa-th"></i>
|
<i class="fa-solid fa-th"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="rpg-inventory-add-btn" data-action="add-location" title="Add new storage location">
|
<button class="rpg-inventory-add-btn" data-action="add-location" title="${i18n.getTranslation('inventory.stored.addLocationTitle') || 'Add new storage location'}">
|
||||||
<i class="fa-solid fa-plus"></i> Add Location
|
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('inventory.stored.addLocationButton') || 'Add Location'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-inventory-content">
|
<div class="rpg-inventory-content">
|
||||||
<div class="rpg-inline-form" id="rpg-add-location-form" style="display: none;">
|
<div class="rpg-inline-form" id="rpg-add-location-form" style="display: none;">
|
||||||
<input type="text" class="rpg-inline-input" id="rpg-new-location-name" placeholder="Enter location name..." />
|
<input type="text" class="rpg-inline-input" id="rpg-new-location-name" placeholder="${i18n.getTranslation('inventory.stored.addLocationPlaceholder') || 'Enter location name...'}" />
|
||||||
<div class="rpg-inline-buttons">
|
<div class="rpg-inline-buttons">
|
||||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-location">
|
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-location">
|
||||||
<i class="fa-solid fa-times"></i> Cancel
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-location">
|
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-location">
|
||||||
<i class="fa-solid fa-check"></i> Save
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.save') || 'Save'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -274,7 +280,7 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
|||||||
if (locations.length === 0) {
|
if (locations.length === 0) {
|
||||||
html += `
|
html += `
|
||||||
<div class="rpg-inventory-empty">
|
<div class="rpg-inventory-empty">
|
||||||
No storage locations yet. Click "Add Location" to create one.
|
${i18n.getTranslation('inventory.stored.empty') || 'No storage locations yet. Click "Add Location" to create one.'}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
@@ -286,30 +292,30 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
|||||||
|
|
||||||
let itemsHtml = '';
|
let itemsHtml = '';
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
itemsHtml = '<div class="rpg-inventory-empty">No items stored here</div>';
|
itemsHtml = '<div class="rpg-inventory-empty">' + (i18n.getTranslation('inventory.stored.noItems') || 'No items stored here') + '</div>';
|
||||||
} else {
|
} else {
|
||||||
if (viewMode === 'grid') {
|
if (viewMode === 'grid') {
|
||||||
// Grid view: card-style items
|
// Grid view: card-style items
|
||||||
itemsHtml = items.map((item, index) => {
|
itemsHtml = items.map((item, index) => {
|
||||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}[${index}]`);
|
const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}.${item}`);
|
||||||
return `
|
return `
|
||||||
<div class="rpg-item-card" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
|
<div class="rpg-item-card" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
|
||||||
${lockIconHtml}
|
${lockIconHtml}
|
||||||
<button class="rpg-item-remove" data-action="remove-item" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Remove item">
|
<button class="rpg-item-remove" data-action="remove-item" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="${i18n.getTranslation('global.removeItem') || 'Remove item'}">
|
||||||
<i class="fa-solid fa-times"></i>
|
<i class="fa-solid fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="${i18n.getTranslation('global.clickToEdit') || 'Click to edit'}">${escapeHtml(item)}</span>
|
||||||
</div>
|
</div>
|
||||||
`}).join('');
|
`}).join('');
|
||||||
} else {
|
} else {
|
||||||
// List view: full-width rows
|
// List view: full-width rows
|
||||||
itemsHtml = items.map((item, index) => {
|
itemsHtml = items.map((item, index) => {
|
||||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}[${index}]`);
|
const lockIconHtml = getLockIconHtml('userStats', `inventory.stored.${location}.${item}`);
|
||||||
return `
|
return `
|
||||||
<div class="rpg-item-row" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
|
<div class="rpg-item-row" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
|
||||||
${lockIconHtml}
|
${lockIconHtml}
|
||||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="${i18n.getTranslation('global.clickToEdit') || 'Click to edit'}">${escapeHtml(item)}</span>
|
||||||
<button class="rpg-item-remove" data-action="remove-item" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Remove item">
|
<button class="rpg-item-remove" data-action="remove-item" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="${i18n.getTranslation('global.removeItem') || 'Remove item'}">
|
||||||
<i class="fa-solid fa-times"></i>
|
<i class="fa-solid fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -327,20 +333,20 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
|||||||
</button>
|
</button>
|
||||||
<h5 class="rpg-storage-name">${escapeHtml(location)}</h5>
|
<h5 class="rpg-storage-name">${escapeHtml(location)}</h5>
|
||||||
<div class="rpg-storage-actions">
|
<div class="rpg-storage-actions">
|
||||||
<button class="rpg-inventory-remove-btn" data-action="remove-location" data-location="${escapeHtml(location)}" title="Remove this storage location">
|
<button class="rpg-inventory-remove-btn" data-action="remove-location" data-location="${escapeHtml(location)}" title="${i18n.getTranslation('inventory.stored.removeLocationTitle') || 'Remove this storage location'}">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-storage-content" ${isCollapsed ? 'style="display:none;"' : ''}>
|
<div class="rpg-storage-content" ${isCollapsed ? 'style="display:none;"' : ''}>
|
||||||
<div class="rpg-inline-form" id="rpg-add-item-form-stored-${locationId}" style="display: none;">
|
<div class="rpg-inline-form" id="rpg-add-item-form-stored-${locationId}" style="display: none;">
|
||||||
<input type="text" class="rpg-inline-input rpg-location-item-input" data-location="${escapeHtml(location)}" placeholder="Enter item name..." />
|
<input type="text" class="rpg-inline-input rpg-location-item-input" data-location="${escapeHtml(location)}" placeholder="${i18n.getTranslation('inventory.addItemPlaceholder') || 'Enter item name...'}" />
|
||||||
<div class="rpg-inline-buttons">
|
<div class="rpg-inline-buttons">
|
||||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="stored" data-location="${escapeHtml(location)}">
|
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="stored" data-location="${escapeHtml(location)}">
|
||||||
<i class="fa-solid fa-times"></i> Cancel
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="stored" data-location="${escapeHtml(location)}">
|
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="stored" data-location="${escapeHtml(location)}">
|
||||||
<i class="fa-solid fa-check"></i> Add
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.add') || 'Add'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -348,19 +354,19 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
|||||||
${itemsHtml}
|
${itemsHtml}
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-storage-add-item-container">
|
<div class="rpg-storage-add-item-container">
|
||||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="stored" data-location="${escapeHtml(location)}" title="Add item to this location">
|
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="stored" data-location="${escapeHtml(location)}" title="${i18n.getTranslation('inventory.stored.addItemToLocationTitle') || 'Add item to this location'}">
|
||||||
<i class="fa-solid fa-plus"></i> Add Item
|
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('inventory.stored.addItemButton') || 'Add Item'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-inline-confirmation" id="rpg-remove-confirm-${locationId}" style="display: none;">
|
<div class="rpg-inline-confirmation" id="rpg-remove-confirm-${locationId}" style="display: none;">
|
||||||
<p>Remove "${escapeHtml(location)}"? This will delete all items stored there.</p>
|
<p>${(i18n.getTranslation('inventory.stored.removeLocationConfirm') || 'Remove "{location}"? This will delete all items stored there.').replace('{location}', escapeHtml(location))}</p>
|
||||||
<div class="rpg-inline-buttons">
|
<div class="rpg-inline-buttons">
|
||||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-remove-location" data-location="${escapeHtml(location)}">
|
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-remove-location" data-location="${escapeHtml(location)}">
|
||||||
<i class="fa-solid fa-times"></i> Cancel
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-inline-btn rpg-inline-confirm" data-action="confirm-remove-location" data-location="${escapeHtml(location)}">
|
<button class="rpg-inline-btn rpg-inline-confirm" data-action="confirm-remove-location" data-location="${escapeHtml(location)}">
|
||||||
<i class="fa-solid fa-check"></i> Confirm
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.confirm') || 'Confirm'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -388,30 +394,30 @@ export function renderAssetsView(assets, viewMode = 'list') {
|
|||||||
|
|
||||||
let itemsHtml = '';
|
let itemsHtml = '';
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
itemsHtml = '<div class="rpg-inventory-empty">No assets owned</div>';
|
itemsHtml = '<div class="rpg-inventory-empty">' + (i18n.getTranslation('inventory.assets.empty') || 'No assets owned') + '</div>';
|
||||||
} else {
|
} else {
|
||||||
if (viewMode === 'grid') {
|
if (viewMode === 'grid') {
|
||||||
// Grid view: card-style items
|
// Grid view: card-style items
|
||||||
itemsHtml = items.map((item, index) => {
|
itemsHtml = items.map((item, index) => {
|
||||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.assets[${index}]`);
|
const lockIconHtml = getLockIconHtml('userStats', `inventory.assets.${item}`);
|
||||||
return `
|
return `
|
||||||
<div class="rpg-item-card" data-field="assets" data-index="${index}">
|
<div class="rpg-item-card" data-field="assets" data-index="${index}">
|
||||||
${lockIconHtml}
|
${lockIconHtml}
|
||||||
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="Remove asset">
|
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="${i18n.getTranslation('inventory.assets.removeAssetTitle') || 'Remove asset'}">
|
||||||
<i class="fa-solid fa-times"></i>
|
<i class="fa-solid fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="assets" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="assets" data-index="${index}" title="${i18n.getTranslation('global.clickToEdit') || 'Click to edit'}">${escapeHtml(item)}</span>
|
||||||
</div>
|
</div>
|
||||||
`}).join('');
|
`}).join('');
|
||||||
} else {
|
} else {
|
||||||
// List view: full-width rows
|
// List view: full-width rows
|
||||||
itemsHtml = items.map((item, index) => {
|
itemsHtml = items.map((item, index) => {
|
||||||
const lockIconHtml = getLockIconHtml('userStats', `inventory.assets[${index}]`);
|
const lockIconHtml = getLockIconHtml('userStats', `inventory.assets.${item}`);
|
||||||
return `
|
return `
|
||||||
<div class="rpg-item-row" data-field="assets" data-index="${index}">
|
<div class="rpg-item-row" data-field="assets" data-index="${index}">
|
||||||
${lockIconHtml}
|
${lockIconHtml}
|
||||||
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="assets" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
|
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="assets" data-index="${index}" title="${i18n.getTranslation('global.clickToEdit') || 'Click to edit'}">${escapeHtml(item)}</span>
|
||||||
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="Remove asset">
|
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="${i18n.getTranslation('inventory.assets.removeAssetTitle') || 'Remove asset'}">
|
||||||
<i class="fa-solid fa-times"></i>
|
<i class="fa-solid fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -424,30 +430,30 @@ export function renderAssetsView(assets, viewMode = 'list') {
|
|||||||
return `
|
return `
|
||||||
<div class="rpg-inventory-section" data-section="assets">
|
<div class="rpg-inventory-section" data-section="assets">
|
||||||
<div class="rpg-inventory-header">
|
<div class="rpg-inventory-header">
|
||||||
<h4>Vehicles, Property & Major Possessions</h4>
|
<h4>${i18n.getTranslation('inventory.assets.title') || 'Vehicles, Property & Major Possessions'}</h4>
|
||||||
<div class="rpg-inventory-header-actions">
|
<div class="rpg-inventory-header-actions">
|
||||||
<div class="rpg-view-toggle">
|
<div class="rpg-view-toggle">
|
||||||
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="assets" data-view="list" title="List view">
|
<button class="rpg-view-btn ${viewMode === 'list' ? 'active' : ''}" data-action="switch-view" data-field="assets" data-view="list" title="${i18n.getTranslation('global.listView') || 'List view'}">
|
||||||
<i class="fa-solid fa-list"></i>
|
<i class="fa-solid fa-list"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="assets" data-view="grid" title="Grid view">
|
<button class="rpg-view-btn ${viewMode === 'grid' ? 'active' : ''}" data-action="switch-view" data-field="assets" data-view="grid" title="${i18n.getTranslation('global.gridView') || 'Grid view'}">
|
||||||
<i class="fa-solid fa-th"></i>
|
<i class="fa-solid fa-th"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="assets" title="Add new asset">
|
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="assets" title="${i18n.getTranslation('inventory.assets.addItemTitle') || 'Add new asset'}">
|
||||||
<i class="fa-solid fa-plus"></i> Add Asset
|
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('inventory.assets.addAssetButton') || 'Add Asset'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-inventory-content">
|
<div class="rpg-inventory-content">
|
||||||
<div class="rpg-inline-form" id="rpg-add-item-form-assets" style="display: none;">
|
<div class="rpg-inline-form" id="rpg-add-item-form-assets" style="display: none;">
|
||||||
<input type="text" class="rpg-inline-input" id="rpg-new-item-assets" placeholder="Enter asset name..." />
|
<input type="text class="rpg-inline-input" id="rpg-new-item-assets" placeholder="${i18n.getTranslation('inventory.assets.addAssetPlaceholder') || 'Enter asset name...'}" />
|
||||||
<div class="rpg-inline-buttons">
|
<div class="rpg-inline-buttons">
|
||||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="assets">
|
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-item" data-field="assets">
|
||||||
<i class="fa-solid fa-times"></i> Cancel
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="assets">
|
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-item" data-field="assets">
|
||||||
<i class="fa-solid fa-check"></i> Add
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.add') || 'Add'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -456,8 +462,7 @@ export function renderAssetsView(assets, viewMode = 'list') {
|
|||||||
</div>
|
</div>
|
||||||
<div class="rpg-inventory-hint">
|
<div class="rpg-inventory-hint">
|
||||||
<i class="fa-solid fa-info-circle"></i>
|
<i class="fa-solid fa-info-circle"></i>
|
||||||
Assets include vehicles (cars, motorcycles), property (homes, apartments),
|
${i18n.getTranslation('inventory.assets.description') || 'Assets include vehicles (cars, motorcycles), property (homes, apartments), and major equipment (workshop tools, special items).'}
|
||||||
and major equipment (workshop tools, special items).
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,9 +3,35 @@
|
|||||||
* Handles UI rendering for quests system (main and optional quests)
|
* Handles UI rendering for quests system (main and optional quests)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { extensionSettings, $questsContainer } from '../../core/state.js';
|
import { extensionSettings, $questsContainer, committedTrackerData, lastGeneratedData } from '../../core/state.js';
|
||||||
import { saveSettings } from '../../core/persistence.js';
|
import { saveSettings, saveChatData } from '../../core/persistence.js';
|
||||||
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
import { isItemLocked, setItemLock } from '../generation/lockManager.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs the current extensionSettings.quests to committedTrackerData.userStats
|
||||||
|
* This ensures quest changes made via UI are reflected in the data sent to AI
|
||||||
|
*/
|
||||||
|
function syncQuestsToCommittedData() {
|
||||||
|
const currentData = committedTrackerData.userStats || lastGeneratedData.userStats;
|
||||||
|
if (!currentData) return;
|
||||||
|
|
||||||
|
const trimmed = currentData.trim();
|
||||||
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const jsonData = JSON.parse(currentData);
|
||||||
|
if (jsonData && typeof jsonData === 'object') {
|
||||||
|
// Update quests in the JSON data
|
||||||
|
jsonData.quests = extensionSettings.quests || { main: 'None', optional: [] };
|
||||||
|
const updatedJSON = JSON.stringify(jsonData, null, 2);
|
||||||
|
committedTrackerData.userStats = updatedJSON;
|
||||||
|
lastGeneratedData.userStats = updatedJSON;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG Quests] Failed to sync quests to committed data:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to generate lock icon HTML if setting is enabled
|
* Helper to generate lock icon HTML if setting is enabled
|
||||||
@@ -19,7 +45,7 @@ function getLockIconHtml(tracker, path) {
|
|||||||
|
|
||||||
const isLocked = isItemLocked(tracker, path);
|
const isLocked = isItemLocked(tracker, path);
|
||||||
const lockIcon = isLocked ? '🔒' : '🔓';
|
const lockIcon = isLocked ? '🔒' : '🔓';
|
||||||
const lockTitle = isLocked ? 'Locked' : 'Unlocked';
|
const lockTitle = isLocked ? i18n.getTranslation('global.locked') || 'Locked' : i18n.getTranslation('global.unlocked') || 'Unlocked';
|
||||||
const lockedClass = isLocked ? ' locked' : '';
|
const lockedClass = isLocked ? ' locked' : '';
|
||||||
return `<span class="rpg-section-lock-icon${lockedClass}" data-tracker="${tracker}" data-path="${path}" title="${lockTitle}">${lockIcon}</span>`;
|
return `<span class="rpg-section-lock-icon${lockedClass}" data-tracker="${tracker}" data-path="${path}" title="${lockTitle}">${lockIcon}</span>`;
|
||||||
}
|
}
|
||||||
@@ -41,13 +67,16 @@ function escapeHtml(text) {
|
|||||||
* @returns {string} HTML for sub-tab navigation
|
* @returns {string} HTML for sub-tab navigation
|
||||||
*/
|
*/
|
||||||
export function renderQuestsSubTabs(activeTab = 'main') {
|
export function renderQuestsSubTabs(activeTab = 'main') {
|
||||||
|
const mainText = i18n.getTranslation('quests.section.main') || 'Main Quest';
|
||||||
|
const optionalText = i18n.getTranslation('quests.section.optional') || 'Optional Quests';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="rpg-quests-subtabs">
|
<div class="rpg-quests-subtabs">
|
||||||
<button class="rpg-quests-subtab ${activeTab === 'main' ? 'active' : ''}" data-tab="main">
|
<button class="rpg-quests-subtab ${activeTab === 'main' ? 'active' : ''}" data-tab="main">
|
||||||
Main Quest
|
${mainText}
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-quests-subtab ${activeTab === 'optional' ? 'active' : ''}" data-tab="optional">
|
<button class="rpg-quests-subtab ${activeTab === 'optional' ? 'active' : ''}" data-tab="optional">
|
||||||
Optional Quests
|
${optionalText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -61,13 +90,18 @@ export function renderQuestsSubTabs(activeTab = 'main') {
|
|||||||
export function renderMainQuestView(mainQuest) {
|
export function renderMainQuestView(mainQuest) {
|
||||||
const questDisplay = (mainQuest && mainQuest !== 'None') ? mainQuest : '';
|
const questDisplay = (mainQuest && mainQuest !== 'None') ? mainQuest : '';
|
||||||
const hasQuest = questDisplay.length > 0;
|
const hasQuest = questDisplay.length > 0;
|
||||||
|
const mainTitle = i18n.getTranslation('quests.main.title') || 'Main Quests';
|
||||||
|
const mainHint = i18n.getTranslation('quests.main.hint') || 'The main quest represents your primary objective in the story.';
|
||||||
|
const mainEmptyText = i18n.getTranslation('quests.main.empty') || 'No active main quests';
|
||||||
|
const addQuestButtonText = i18n.getTranslation('quests.main.addQuestButton') || 'Add Quest';
|
||||||
|
const addQuestPlaceholderText = i18n.getTranslation('quests.main.addQuestPlaceholder') || 'Enter main quest title...';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="rpg-quest-section">
|
<div class="rpg-quest-section">
|
||||||
<div class="rpg-quest-header">
|
<div class="rpg-quest-header">
|
||||||
<h3 class="rpg-quest-section-title">Main Quests</h3>
|
<h3 class="rpg-quest-section-title">${mainTitle}</h3>
|
||||||
${!hasQuest ? `<button class="rpg-add-quest-btn" data-action="add-quest" data-field="main" title="Add main quests">
|
${!hasQuest ? `<button class="rpg-add-quest-btn" data-action="add-quest" data-field="main" title="${i18n.getTranslation('quests.main.addQuestTitle') || 'Add main quests'}">
|
||||||
<i class="fa-solid fa-plus"></i> Add Quest
|
<i class="fa-solid fa-plus"></i> ${addQuestButtonText}
|
||||||
</button>` : ''}
|
</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-quest-content">
|
<div class="rpg-quest-content">
|
||||||
@@ -76,10 +110,10 @@ export function renderMainQuestView(mainQuest) {
|
|||||||
<input type="text" class="rpg-inline-input" id="rpg-edit-quest-main" value="${escapeHtml(questDisplay)}" />
|
<input type="text" class="rpg-inline-input" id="rpg-edit-quest-main" value="${escapeHtml(questDisplay)}" />
|
||||||
<div class="rpg-inline-buttons">
|
<div class="rpg-inline-buttons">
|
||||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-edit-quest" data-field="main">
|
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-edit-quest" data-field="main">
|
||||||
<i class="fa-solid fa-times"></i> Cancel
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-edit-quest" data-field="main">
|
<button class="rpg-inline-btn rpg-inline-save" data-action="save-edit-quest" data-field="main">
|
||||||
<i class="fa-solid fa-check"></i> Save
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.save') || 'Save'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,32 +121,32 @@ export function renderMainQuestView(mainQuest) {
|
|||||||
${getLockIconHtml('userStats', 'quests.main')}
|
${getLockIconHtml('userStats', 'quests.main')}
|
||||||
<div class="rpg-quest-title">${escapeHtml(questDisplay)}</div>
|
<div class="rpg-quest-title">${escapeHtml(questDisplay)}</div>
|
||||||
<div class="rpg-quest-actions">
|
<div class="rpg-quest-actions">
|
||||||
<button class="rpg-quest-edit" data-action="edit-quest" data-field="main" title="Edit quest">
|
<button class="rpg-quest-edit" data-action="edit-quest" data-field="main" title="${i18n.getTranslation('quests.editQuestTitle') || 'Edit quest'}">
|
||||||
<i class="fa-solid fa-edit"></i>
|
<i class="fa-solid fa-edit"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-quest-remove" data-action="remove-quest" data-field="main" title="Complete/Remove quest">
|
<button class="rpg-quest-remove" data-action="remove-quest" data-field="main" title="${i18n.getTranslation('quests.removeQuestTitle') || 'Complete/Remove quest'}">
|
||||||
<i class="fa-solid fa-check"></i>
|
<i class="fa-solid fa-check"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
` : `
|
` : `
|
||||||
<div class="rpg-inline-form" id="rpg-add-quest-form-main" style="display: none;">
|
<div class="rpg-inline-form" id="rpg-add-quest-form-main" style="display: none;">
|
||||||
<input type="text" class="rpg-inline-input" id="rpg-new-quest-main" placeholder="Enter main quests title..." />
|
<input type="text" class="rpg-inline-input" id="rpg-new-quest-main" placeholder="${addQuestPlaceholderText}" />
|
||||||
<div class="rpg-inline-actions">
|
<div class="rpg-inline-actions">
|
||||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="main">
|
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="main">
|
||||||
<i class="fa-solid fa-times"></i> Cancel
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-quest" data-field="main">
|
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-quest" data-field="main">
|
||||||
<i class="fa-solid fa-check"></i> Add
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.add') || 'Add'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-quest-empty">No active main quests</div>
|
<div class="rpg-quest-empty">${mainEmptyText}</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-quest-hint">
|
<div class="rpg-quest-hint">
|
||||||
<i class="fa-solid fa-lightbulb"></i>
|
<i class="fa-solid fa-lightbulb"></i>
|
||||||
The main quests represent your primary objective in the story.
|
${mainHint}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -125,18 +159,23 @@ export function renderMainQuestView(mainQuest) {
|
|||||||
*/
|
*/
|
||||||
export function renderOptionalQuestsView(optionalQuests) {
|
export function renderOptionalQuestsView(optionalQuests) {
|
||||||
const quests = optionalQuests.filter(q => q && q !== 'None');
|
const quests = optionalQuests.filter(q => q && q !== 'None');
|
||||||
|
const optionalTitle = i18n.getTranslation('quests.optional.title') || 'Optional Quests';
|
||||||
|
const optionalHint = i18n.getTranslation('quests.optional.hint') || 'Optional quests are side objectives that complement your main story.';
|
||||||
|
const optionalEmptyText = i18n.getTranslation('quests.optional.empty') || 'No active optional quests';
|
||||||
|
const addQuestButtonText = i18n.getTranslation('quests.optional.addQuestButton') || 'Add Quest';
|
||||||
|
const addQuestPlaceholderText = i18n.getTranslation('quests.optional.addQuestPlaceholder') || 'Enter optional quest title...';
|
||||||
|
|
||||||
let questsHtml = '';
|
let questsHtml = '';
|
||||||
if (quests.length === 0) {
|
if (quests.length === 0) {
|
||||||
questsHtml = '<div class="rpg-quest-empty">No active optional quests</div>';
|
questsHtml = `<div class="rpg-quest-empty">${optionalEmptyText}</div>`;
|
||||||
} else {
|
} else {
|
||||||
questsHtml = quests.map((quest, index) => {
|
questsHtml = quests.map((quest, index) => {
|
||||||
return `
|
return `
|
||||||
<div class="rpg-quest-item" data-field="optional" data-index="${index}">
|
<div class="rpg-quest-item" data-field="optional" data-index="${index}">
|
||||||
${getLockIconHtml('userStats', `quests.optional[${index}]`)}
|
${getLockIconHtml('userStats', `quests.optional[${index}]`)}
|
||||||
<div class="rpg-quest-title rpg-editable" contenteditable="true" data-field="optional" data-index="${index}" title="Click to edit">${escapeHtml(quest)}</div>
|
<div class="rpg-quest-title rpg-editable" contenteditable="true" data-field="optional" data-index="${index}" title="${i18n.getTranslation('global.clickToEdit') || 'Click to edit'}">${escapeHtml(quest)}</div>
|
||||||
<div class="rpg-quest-actions">
|
<div class="rpg-quest-actions">
|
||||||
<button class="rpg-quest-remove" data-action="remove-quest" data-field="optional" data-index="${index}" title="Complete/Remove quest">
|
<button class="rpg-quest-remove" data-action="remove-quest" data-field="optional" data-index="${index}" title="${i18n.getTranslation('quests.removeQuestTitle') || 'Complete/Remove quest'}">
|
||||||
<i class="fa-solid fa-check"></i>
|
<i class="fa-solid fa-check"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,20 +186,20 @@ export function renderOptionalQuestsView(optionalQuests) {
|
|||||||
return `
|
return `
|
||||||
<div class="rpg-quest-section">
|
<div class="rpg-quest-section">
|
||||||
<div class="rpg-quest-header">
|
<div class="rpg-quest-header">
|
||||||
<h3 class="rpg-quest-section-title">Optional Quests</h3>
|
<h3 class="rpg-quest-section-title">${optionalTitle}</h3>
|
||||||
<button class="rpg-add-quest-btn" data-action="add-quest" data-field="optional" title="Add optional quest">
|
<button class="rpg-add-quest-btn" data-action="add-quest" data-field="optional" title="${i18n.getTranslation('quests.optional.addQuestTitle') || 'Add optional quest'}">
|
||||||
<i class="fa-solid fa-plus"></i> Add Quest
|
<i class="fa-solid fa-plus"></i> ${addQuestButtonText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-quest-content">
|
<div class="rpg-quest-content">
|
||||||
<div class="rpg-inline-form" id="rpg-add-quest-form-optional" style="display: none;">
|
<div class="rpg-inline-form" id="rpg-add-quest-form-optional" style="display: none;">
|
||||||
<input type="text" class="rpg-inline-input" id="rpg-new-quest-optional" placeholder="Enter optional quest title..." />
|
<input type="text" class="rpg-inline-input" id="rpg-new-quest-optional" placeholder="${addQuestPlaceholderText}" />
|
||||||
<div class="rpg-inline-buttons">
|
<div class="rpg-inline-buttons">
|
||||||
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="optional">
|
<button class="rpg-inline-btn rpg-inline-cancel" data-action="cancel-add-quest" data-field="optional">
|
||||||
<i class="fa-solid fa-times"></i> Cancel
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
</button>
|
</button>
|
||||||
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-quest" data-field="optional">
|
<button class="rpg-inline-btn rpg-inline-save" data-action="save-add-quest" data-field="optional">
|
||||||
<i class="fa-solid fa-check"></i> Add
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('global.add') || 'Add'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,7 +208,7 @@ export function renderOptionalQuestsView(optionalQuests) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="rpg-quest-hint">
|
<div class="rpg-quest-hint">
|
||||||
<i class="fa-solid fa-info-circle"></i>
|
<i class="fa-solid fa-info-circle"></i>
|
||||||
Optional quests are side objectives that complement your main story.
|
${optionalHint}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,15 +219,19 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current sub-tab from container or default to 'main'
|
// Get current sub-tab from container or default to 'main'
|
||||||
const activeSubTab = $questsContainer.data('active-subtab') || 'main';
|
const activeSubTab = $questsContainer.data('active-subtab') || 'main';
|
||||||
|
|
||||||
// Get quests data
|
// Get quests data - extract value if it's a locked object
|
||||||
const mainQuest = extensionSettings.quests.main || 'None';
|
let mainQuest = extensionSettings.quests.main || 'None';
|
||||||
|
// Recursively extract value if it's nested objects
|
||||||
|
while (typeof mainQuest === 'object' && mainQuest.value !== undefined) {
|
||||||
|
mainQuest = mainQuest.value;
|
||||||
|
}
|
||||||
const optionalQuests = extensionSettings.quests.optional || [];
|
const optionalQuests = extensionSettings.quests.optional || [];
|
||||||
|
|
||||||
// Build HTML
|
// Build HTML
|
||||||
@@ -250,7 +293,10 @@ function attachQuestEventHandlers() {
|
|||||||
}
|
}
|
||||||
extensionSettings.quests.optional.push(questTitle);
|
extensionSettings.quests.optional.push(questTitle);
|
||||||
}
|
}
|
||||||
|
// Sync quest changes to committedTrackerData so AI sees the addition
|
||||||
|
syncQuestsToCommittedData();
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
saveChatData();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -278,7 +324,10 @@ function attachQuestEventHandlers() {
|
|||||||
|
|
||||||
if (questTitle) {
|
if (questTitle) {
|
||||||
extensionSettings.quests.main = questTitle;
|
extensionSettings.quests.main = questTitle;
|
||||||
|
// Sync quest changes to committedTrackerData so AI sees the edit
|
||||||
|
syncQuestsToCommittedData();
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
saveChatData();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -293,7 +342,10 @@ function attachQuestEventHandlers() {
|
|||||||
} else {
|
} else {
|
||||||
extensionSettings.quests.optional.splice(index, 1);
|
extensionSettings.quests.optional.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
// Sync quest changes to committedTrackerData so AI sees the removal
|
||||||
|
syncQuestsToCommittedData();
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
saveChatData();
|
||||||
renderQuests();
|
renderQuests();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -306,7 +358,10 @@ function attachQuestEventHandlers() {
|
|||||||
|
|
||||||
if (newTitle && field === 'optional' && index !== undefined) {
|
if (newTitle && field === 'optional' && index !== undefined) {
|
||||||
extensionSettings.quests.optional[index] = newTitle;
|
extensionSettings.quests.optional[index] = newTitle;
|
||||||
|
// Sync quest changes to committedTrackerData so AI sees the edit
|
||||||
|
syncQuestsToCommittedData();
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
saveChatData();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+886
-430
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ import {
|
|||||||
$userStatsContainer,
|
$userStatsContainer,
|
||||||
FALLBACK_AVATAR_DATA_URI
|
FALLBACK_AVATAR_DATA_URI
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
import {
|
import {
|
||||||
saveSettings,
|
saveSettings,
|
||||||
saveChatData,
|
saveChatData,
|
||||||
@@ -20,6 +21,23 @@ import {
|
|||||||
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
|
||||||
import { buildInventorySummary } from '../generation/promptBuilder.js';
|
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 { 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.
|
||||||
|
* Example: "Conditions (up to 5 traits)" -> "conditions"
|
||||||
|
* @param {string} name - Field name, possibly with parenthetical description
|
||||||
|
* @returns {string} snake_case key from the base name only
|
||||||
|
*/
|
||||||
|
function toFieldKey(name) {
|
||||||
|
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
|
||||||
|
return baseName
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\p{L}\p{N}]+/gu, '_')
|
||||||
|
.replace(/^_+|_+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the user stats text string using custom stat names
|
* Builds the user stats text string using custom stat names
|
||||||
@@ -104,7 +122,8 @@ function updateUserStatsData() {
|
|||||||
|
|
||||||
// Then, add any other numeric stats from extensionSettings that aren't in config
|
// Then, add any other numeric stats from extensionSettings that aren't in config
|
||||||
// (these could be custom stats the AI added or disabled stats)
|
// (these could be custom stats the AI added or disabled stats)
|
||||||
const excludeFields = new Set(['mood', 'conditions', 'inventory', 'skills', 'level']);
|
const customFields = config.statusSection?.customFields || [];
|
||||||
|
const excludeFields = new Set(['mood', ...customFields.map(f => toFieldKey(f)), 'inventory', 'skills', 'level']);
|
||||||
Object.entries(stats).forEach(([key, value]) => {
|
Object.entries(stats).forEach(([key, value]) => {
|
||||||
if (!processedIds.has(key) && !excludeFields.has(key) && typeof value === 'number') {
|
if (!processedIds.has(key) && !excludeFields.has(key) && typeof value === 'number') {
|
||||||
statsArray.push({
|
statsArray.push({
|
||||||
@@ -117,12 +136,17 @@ function updateUserStatsData() {
|
|||||||
|
|
||||||
jsonData.stats = statsArray;
|
jsonData.stats = statsArray;
|
||||||
|
|
||||||
// Update status
|
// Update status - include all custom status fields
|
||||||
jsonData.status = {
|
jsonData.status = {
|
||||||
mood: stats.mood || '😐',
|
mood: stats.mood || '😐'
|
||||||
conditions: stats.conditions || 'None'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add all custom status fields
|
||||||
|
for (const fieldName of customFields) {
|
||||||
|
const fieldKey = toFieldKey(fieldName);
|
||||||
|
jsonData.status[fieldKey] = stats[fieldKey] || 'None';
|
||||||
|
}
|
||||||
|
|
||||||
// Update inventory (convert to v3 format)
|
// Update inventory (convert to v3 format)
|
||||||
const convertToV3Items = (itemString) => {
|
const convertToV3Items = (itemString) => {
|
||||||
if (!itemString) return [];
|
if (!itemString) return [];
|
||||||
@@ -190,7 +214,7 @@ export function renderUserStats() {
|
|||||||
|
|
||||||
if (!lastGeneratedData.userStats && !committedTrackerData.userStats) {
|
if (!lastGeneratedData.userStats && !committedTrackerData.userStats) {
|
||||||
// Always render to the #rpg-user-stats container (mobile layout just moves it around in DOM)
|
// Always render to the #rpg-user-stats container (mobile layout just moves it around in DOM)
|
||||||
$userStatsContainer.html('<div class="rpg-inventory-empty">No statuses generated yet</div>');
|
$userStatsContainer.html('<div class="rpg-inventory-empty">' + (i18n.getTranslation('userStats.empty') || 'No statuses generated yet') + '</div>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,13 +268,14 @@ export function renderUserStats() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create gradient from low to high color
|
// Create gradient from low to high color with opacity
|
||||||
const gradient = `linear-gradient(to right, ${extensionSettings.statBarColorLow}, ${extensionSettings.statBarColorHigh})`;
|
const colors = getStatBarColors();
|
||||||
|
const gradient = `linear-gradient(to right, ${colors.low}, ${colors.high})`;
|
||||||
|
|
||||||
// Check if stats bars section is locked
|
// Check if stats bars section is locked
|
||||||
const isStatsLocked = isItemLocked('userStats', 'stats');
|
const isStatsLocked = isItemLocked('userStats', 'stats');
|
||||||
const lockIcon = isStatsLocked ? '🔒' : '🔓';
|
const lockIcon = isStatsLocked ? '🔒' : '🔓';
|
||||||
const lockTitle = isStatsLocked ? 'Locked - AI cannot change stats' : 'Unlocked - AI can change stats';
|
const lockTitle = isStatsLocked ? (i18n.getTranslation('userStats.statsLocked') || 'Stats locked') : (i18n.getTranslation('userStats.statsUnlocked') || 'Stats unlocked');
|
||||||
const lockedClass = isStatsLocked ? ' locked' : '';
|
const lockedClass = isStatsLocked ? ' locked' : '';
|
||||||
|
|
||||||
let html = '<div class="rpg-stats-content">';
|
let html = '<div class="rpg-stats-content">';
|
||||||
@@ -263,8 +288,8 @@ export function renderUserStats() {
|
|||||||
<img src="${userPortrait}" alt="${userName}" class="rpg-user-portrait" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
<img src="${userPortrait}" alt="${userName}" class="rpg-user-portrait" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
||||||
<span class="rpg-user-name">${userName}</span>
|
<span class="rpg-user-name">${userName}</span>
|
||||||
${showLevel ? `<span style="opacity: 0.5;">|</span>
|
${showLevel ? `<span style="opacity: 0.5;">|</span>
|
||||||
<span class="rpg-level-label">LVL</span>
|
<span class="rpg-level-label">${i18n.getTranslation('userStats.level') || 'Level'}</span>
|
||||||
<span class="rpg-level-value rpg-editable" contenteditable="true" data-field="level" title="Click to edit level">${extensionSettings.level}</span>` : ''}
|
<span class="rpg-level-value rpg-editable" contenteditable="true" data-field="level" title="${i18n.getTranslation('userStats.clickToEditLevel') || 'Click to edit level'}">${extensionSettings.level}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -275,16 +300,33 @@ export function renderUserStats() {
|
|||||||
}
|
}
|
||||||
html += '<div class="rpg-stats-grid">';
|
html += '<div class="rpg-stats-grid">';
|
||||||
const enabledStats = config.customStats.filter(stat => stat && stat.enabled && stat.name && stat.id);
|
const enabledStats = config.customStats.filter(stat => stat && stat.enabled && stat.name && stat.id);
|
||||||
|
const displayMode = config.statsDisplayMode || 'percentage';
|
||||||
|
|
||||||
for (const stat of enabledStats) {
|
for (const stat of enabledStats) {
|
||||||
const value = stats[stat.id] !== undefined ? stats[stat.id] : 100;
|
const value = stats[stat.id] !== undefined ? stats[stat.id] : 100;
|
||||||
|
const maxValue = stat.maxValue || 100;
|
||||||
|
|
||||||
|
// Calculate percentage for bar fill
|
||||||
|
let percentage;
|
||||||
|
let displayValue;
|
||||||
|
|
||||||
|
if (displayMode === 'number') {
|
||||||
|
// In number mode, value is already the number (0 to maxValue)
|
||||||
|
percentage = maxValue > 0 ? (value / maxValue) * 100 : 100;
|
||||||
|
displayValue = `${value}/${maxValue}`;
|
||||||
|
} else {
|
||||||
|
// In percentage mode, value is 0-100
|
||||||
|
percentage = value;
|
||||||
|
displayValue = `${value}%`;
|
||||||
|
}
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<div class="rpg-stat-row">
|
<div class="rpg-stat-row">
|
||||||
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="${stat.id}" title="Click to edit stat name">${stat.name}:</span>
|
<span class="rpg-stat-label rpg-editable-stat-name" contenteditable="true" data-field="${stat.id}" title="${i18n.getTranslation('userStats.clickToEditStatName') || 'Click to edit stat name'}">${stat.name}:</span>
|
||||||
<div class="rpg-stat-bar" style="background: ${gradient}">
|
<div class="rpg-stat-bar" style="background: ${gradient}">
|
||||||
<div class="rpg-stat-fill" style="width: ${100 - value}%"></div>
|
<div class="rpg-stat-fill" style="width: ${100 - percentage}%"></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="${stat.id}" title="Click to edit">${value}%</span>
|
<span class="rpg-stat-value rpg-editable-stat" contenteditable="true" data-field="${stat.id}" data-max="${maxValue}" data-mode="${displayMode}" title="${i18n.getTranslation('userStats.clickToEditStatValue') || 'Click to edit stat value'}">${displayValue}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -294,7 +336,7 @@ export function renderUserStats() {
|
|||||||
if (config.statusSection.enabled) {
|
if (config.statusSection.enabled) {
|
||||||
const isMoodLocked = isItemLocked('userStats', 'status');
|
const isMoodLocked = isItemLocked('userStats', 'status');
|
||||||
const moodLockIcon = isMoodLocked ? '🔒' : '🔓';
|
const moodLockIcon = isMoodLocked ? '🔒' : '🔓';
|
||||||
const moodLockTitle = isMoodLocked ? 'Locked - AI cannot change mood' : 'Unlocked - AI can change mood';
|
const moodLockTitle = isMoodLocked ? (i18n.getTranslation('userStats.moodLocked') || 'Mood locked') : (i18n.getTranslation('userStats.moodUnlocked') || 'Mood unlocked');
|
||||||
const moodLockedClass = isMoodLocked ? ' locked' : '';
|
const moodLockedClass = isMoodLocked ? ' locked' : '';
|
||||||
html += '<div class="rpg-mood">';
|
html += '<div class="rpg-mood">';
|
||||||
if (showLockIcons) {
|
if (showLockIcons) {
|
||||||
@@ -302,18 +344,23 @@ export function renderUserStats() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.statusSection.showMoodEmoji) {
|
if (config.statusSection.showMoodEmoji) {
|
||||||
html += `<div class="rpg-mood-emoji rpg-editable" contenteditable="true" data-field="mood" title="Click to edit emoji">${stats.mood}</div>`;
|
html += `<div class="rpg-mood-emoji rpg-editable" contenteditable="true" data-field="mood" title="${i18n.getTranslation('userStats.clickToEditEmoji') || 'Click to edit emoji'}">${stats.mood}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render custom status fields
|
// Render custom status fields
|
||||||
if (config.statusSection.customFields && config.statusSection.customFields.length > 0) {
|
if (config.statusSection.customFields && config.statusSection.customFields.length > 0) {
|
||||||
// For now, use first field as "conditions" for backward compatibility
|
for (const fieldName of config.statusSection.customFields) {
|
||||||
let conditionsValue = stats.conditions || 'None';
|
const fieldKey = toFieldKey(fieldName);
|
||||||
// Strip brackets if present (from JSON array format)
|
let fieldValue = stats[fieldKey] || 'None';
|
||||||
if (typeof conditionsValue === 'string') {
|
// Handle array format (from JSON)
|
||||||
conditionsValue = conditionsValue.replace(/^\[|\]$/g, '').trim();
|
if (Array.isArray(fieldValue)) {
|
||||||
|
fieldValue = fieldValue.join(', ') || 'None';
|
||||||
|
} else if (typeof fieldValue === 'string') {
|
||||||
|
// Strip brackets if present (from JSON array format)
|
||||||
|
fieldValue = fieldValue.replace(/^\[|\]$/g, '').trim();
|
||||||
|
}
|
||||||
|
html += `<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="${fieldKey}" title="${i18n.getTranslation('userStats.clickToEdit') || 'Click to edit'} ${fieldName}">${fieldValue}</div>`;
|
||||||
}
|
}
|
||||||
html += `<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="conditions" title="Click to edit conditions">${conditionsValue}</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
@@ -323,7 +370,7 @@ export function renderUserStats() {
|
|||||||
if (config.skillsSection.enabled) {
|
if (config.skillsSection.enabled) {
|
||||||
const isSkillsLocked = isItemLocked('userStats', 'skills');
|
const isSkillsLocked = isItemLocked('userStats', 'skills');
|
||||||
const skillsLockIcon = isSkillsLocked ? '🔒' : '🔓';
|
const skillsLockIcon = isSkillsLocked ? '🔒' : '🔓';
|
||||||
const skillsLockTitle = isSkillsLocked ? 'Locked - AI cannot change skills' : 'Unlocked - AI can change skills';
|
const skillsLockTitle = isSkillsLocked ? (i18n.getTranslation('userStats.skillsLocked') || 'Skills locked') : (i18n.getTranslation('userStats.skillsUnlocked') || 'Skills unlocked');
|
||||||
const skillsLockedClass = isSkillsLocked ? ' locked' : '';
|
const skillsLockedClass = isSkillsLocked ? ' locked' : '';
|
||||||
let skillsValue = 'None';
|
let skillsValue = 'None';
|
||||||
// Handle JSON array format: [{name: "Art"}, {name: "Coding"}]
|
// Handle JSON array format: [{name: "Art"}, {name: "Coding"}]
|
||||||
@@ -340,7 +387,7 @@ export function renderUserStats() {
|
|||||||
}
|
}
|
||||||
html += `
|
html += `
|
||||||
<span class="rpg-skills-label">${config.skillsSection.label}:</span>
|
<span class="rpg-skills-label">${config.skillsSection.label}:</span>
|
||||||
<div class="rpg-skills-value rpg-editable" contenteditable="true" data-field="skills" title="Click to edit skills">${skillsValue}</div>
|
<div class="rpg-skills-value rpg-editable" contenteditable="true" data-field="skills" title="${i18n.getTranslation('userStats.clickToEditSkills') || 'Click to edit skills'}">${skillsValue}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -364,27 +411,30 @@ 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) {
|
||||||
html += `
|
const equipmentBonuses = getEquipmentBonuses();
|
||||||
|
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;
|
||||||
html += `
|
const bonus = equipmentBonuses[attr.id] || 0;
|
||||||
|
const bonusHtml = bonus > 0 ? `<span class="rpg-classic-stat-bonus" title="Equipment bonus: +${bonus}"> +${bonus}</span>` : '';
|
||||||
|
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>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -403,16 +453,33 @@ export function renderUserStats() {
|
|||||||
// console.log('[RPG UserStats Render] ✓ HTML rendered to #rpg-user-stats container');
|
// console.log('[RPG UserStats Render] ✓ HTML rendered to #rpg-user-stats container');
|
||||||
|
|
||||||
// Add event listeners for editable stat values
|
// Add event listeners for editable stat values
|
||||||
$('.rpg-editable-stat').on('blur', function() {
|
$('.rpg-editable-stat').on('blur', function () {
|
||||||
const field = $(this).data('field');
|
const field = $(this).data('field');
|
||||||
const textValue = $(this).text().replace('%', '').trim();
|
const mode = $(this).data('mode');
|
||||||
let value = parseInt(textValue);
|
const maxValue = parseInt($(this).data('max')) || 100;
|
||||||
|
const textValue = $(this).text().trim();
|
||||||
|
let value;
|
||||||
|
|
||||||
// Validate and clamp value between 0 and 100
|
if (mode === 'number') {
|
||||||
if (isNaN(value)) {
|
// In number mode, parse "X/MAX" or just "X"
|
||||||
value = 0;
|
const parts = textValue.split('/');
|
||||||
|
value = parseInt(parts[0]);
|
||||||
|
|
||||||
|
// Validate and clamp value between 0 and maxValue
|
||||||
|
if (isNaN(value)) {
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
value = Math.max(0, Math.min(maxValue, value));
|
||||||
|
} else {
|
||||||
|
// In percentage mode, parse "X%" or just "X"
|
||||||
|
value = parseInt(textValue.replace('%', ''));
|
||||||
|
|
||||||
|
// Validate and clamp value between 0 and 100
|
||||||
|
if (isNaN(value)) {
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
value = Math.max(0, Math.min(100, value));
|
||||||
}
|
}
|
||||||
value = Math.max(0, Math.min(100, value));
|
|
||||||
|
|
||||||
// Update the setting
|
// Update the setting
|
||||||
extensionSettings.userStats[field] = value;
|
extensionSettings.userStats[field] = value;
|
||||||
@@ -424,12 +491,13 @@ export function renderUserStats() {
|
|||||||
saveChatData();
|
saveChatData();
|
||||||
updateMessageSwipeData();
|
updateMessageSwipeData();
|
||||||
|
|
||||||
// Re-render to update the bar
|
// Re-render to update the bar and FAB widgets
|
||||||
renderUserStats();
|
renderUserStats();
|
||||||
|
updateFabWidgets();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add event listeners for mood/conditions editing
|
// Add event listeners for mood/conditions editing
|
||||||
$('.rpg-mood-emoji.rpg-editable').on('blur', function() {
|
$('.rpg-mood-emoji.rpg-editable').on('blur', function () {
|
||||||
const value = $(this).text().trim();
|
const value = $(this).text().trim();
|
||||||
extensionSettings.userStats.mood = value || '😐';
|
extensionSettings.userStats.mood = value || '😐';
|
||||||
|
|
||||||
@@ -441,9 +509,10 @@ export function renderUserStats() {
|
|||||||
updateMessageSwipeData();
|
updateMessageSwipeData();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.rpg-mood-conditions.rpg-editable').on('blur', function() {
|
$('.rpg-mood-conditions.rpg-editable').on('blur', function () {
|
||||||
const value = $(this).text().trim();
|
const value = $(this).text().trim();
|
||||||
extensionSettings.userStats.conditions = value || 'None';
|
const fieldKey = $(this).data('field');
|
||||||
|
extensionSettings.userStats[fieldKey] = value || 'None';
|
||||||
|
|
||||||
// Update userStats data (maintains JSON or text format)
|
// Update userStats data (maintains JSON or text format)
|
||||||
updateUserStatsData();
|
updateUserStatsData();
|
||||||
@@ -454,7 +523,7 @@ export function renderUserStats() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add event listener for skills editing
|
// Add event listener for skills editing
|
||||||
$('.rpg-skills-value.rpg-editable').on('blur', function() {
|
$('.rpg-skills-value.rpg-editable').on('blur', function () {
|
||||||
const value = $(this).text().trim();
|
const value = $(this).text().trim();
|
||||||
extensionSettings.userStats.skills = value || 'None';
|
extensionSettings.userStats.skills = value || 'None';
|
||||||
|
|
||||||
@@ -467,7 +536,7 @@ export function renderUserStats() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add event listeners for stat name editing
|
// Add event listeners for stat name editing
|
||||||
$('.rpg-editable-stat-name').on('blur', function() {
|
$('.rpg-editable-stat-name').on('blur', function () {
|
||||||
const field = $(this).data('field');
|
const field = $(this).data('field');
|
||||||
const value = $(this).text().trim().replace(':', '');
|
const value = $(this).text().trim().replace(':', '');
|
||||||
|
|
||||||
@@ -491,7 +560,7 @@ export function renderUserStats() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add event listener for level editing
|
// Add event listener for level editing
|
||||||
$('.rpg-level-value.rpg-editable').on('blur', function() {
|
$('.rpg-level-value.rpg-editable').on('blur', function () {
|
||||||
let value = parseInt($(this).text().trim());
|
let value = parseInt($(this).text().trim());
|
||||||
if (isNaN(value) || value < 1) {
|
if (isNaN(value) || value < 1) {
|
||||||
value = 1;
|
value = 1;
|
||||||
@@ -509,15 +578,15 @@ export function renderUserStats() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Prevent line breaks in level field
|
// Prevent line breaks in level field
|
||||||
$('.rpg-level-value.rpg-editable').on('keydown', function(e) {
|
$('.rpg-level-value.rpg-editable').on('keydown', function (e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$(this).blur();
|
$(this).blur();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add event listener for section lock icon clicks (support both click and touch)
|
// Add event listener for section lock icon clicks (support both click and touch)
|
||||||
$('.rpg-section-lock-icon').on('click touchend', function(e) {
|
$('.rpg-section-lock-icon').on('click touchend', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const $icon = $(this);
|
const $icon = $(this);
|
||||||
@@ -530,7 +599,7 @@ export function renderUserStats() {
|
|||||||
|
|
||||||
// Update icon
|
// Update icon
|
||||||
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
||||||
const newTitle = !currentlyLocked ? 'Locked - AI cannot change this section' : 'Unlocked - AI can change this section';
|
const newTitle = !currentlyLocked ? (i18n.getTranslation('infoBox.locked') || 'Locked') : (i18n.getTranslation('infoBox.unlocked') || 'Unlocked');
|
||||||
$icon.text(newIcon);
|
$icon.text(newIcon);
|
||||||
$icon.attr('title', newTitle);
|
$icon.attr('title', newTitle);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { extensionSettings } from '../../core/state.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { getThoughtBasedExpressionPortraitForCharacter } from '../../utils/thoughtBasedExpressionPortraits.js';
|
||||||
|
import { getSafeImageSrc } from '../../utils/imageUrls.js';
|
||||||
|
import {
|
||||||
|
getPresentCharactersTrackerData,
|
||||||
|
parsePresentCharacters,
|
||||||
|
resolvePresentCharacterPortrait
|
||||||
|
} from '../../utils/presentCharacters.js';
|
||||||
|
|
||||||
|
const PANEL_ID = 'rpg-alt-present-characters';
|
||||||
|
|
||||||
|
function ensureAlternatePresentCharactersPanel() {
|
||||||
|
let $panel = $(`#${PANEL_ID}`);
|
||||||
|
if ($panel.length) {
|
||||||
|
return $panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
$panel = $(`<div id="${PANEL_ID}" class="rpg-alt-present-characters" style="display:none;"></div>`);
|
||||||
|
|
||||||
|
const $sendForm = $('#send_form');
|
||||||
|
const $sheld = $('#sheld');
|
||||||
|
const $chat = $sheld.find('#chat');
|
||||||
|
|
||||||
|
if ($sendForm.length) {
|
||||||
|
$sendForm.before($panel);
|
||||||
|
} else if ($chat.length) {
|
||||||
|
$chat.after($panel);
|
||||||
|
} else if ($sheld.length) {
|
||||||
|
$sheld.append($panel);
|
||||||
|
} else {
|
||||||
|
$('body').append($panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToRgba(hex, opacity = 100) {
|
||||||
|
const r = parseInt(hex.slice(1, 3), 16);
|
||||||
|
const g = parseInt(hex.slice(3, 5), 16);
|
||||||
|
const b = parseInt(hex.slice(5, 7), 16);
|
||||||
|
const a = opacity / 100;
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePortraitLoadError() {
|
||||||
|
this.style.opacity = '0.5';
|
||||||
|
$(this).off('error', handlePortraitLoadError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAlternatePresentCharacterCard(character) {
|
||||||
|
const rawPortrait = (extensionSettings.enableThoughtBasedExpressions
|
||||||
|
? getThoughtBasedExpressionPortraitForCharacter(character.name)
|
||||||
|
: null) || resolvePresentCharacterPortrait(character.name);
|
||||||
|
const portrait = getSafeImageSrc(rawPortrait);
|
||||||
|
const name = String(character.name || '');
|
||||||
|
|
||||||
|
const $card = $('<div class="rpg-alt-present-character"></div>')
|
||||||
|
.attr('data-character-name', name)
|
||||||
|
.attr('title', name);
|
||||||
|
|
||||||
|
const $portrait = $('<div class="rpg-alt-present-character__portrait"></div>');
|
||||||
|
const $image = $('<img />')
|
||||||
|
.attr({
|
||||||
|
alt: name,
|
||||||
|
loading: 'lazy'
|
||||||
|
})
|
||||||
|
.on('error', handlePortraitLoadError);
|
||||||
|
|
||||||
|
if (portrait) {
|
||||||
|
$image.attr('src', portrait);
|
||||||
|
}
|
||||||
|
|
||||||
|
const $meta = $('<div class="rpg-alt-present-character__meta"></div>');
|
||||||
|
const $name = $('<div class="rpg-alt-present-character__name"></div>').text(name);
|
||||||
|
|
||||||
|
$portrait.append($image);
|
||||||
|
$meta.append($name);
|
||||||
|
$card.append($portrait, $meta);
|
||||||
|
|
||||||
|
return $card;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeAlternatePresentCharactersPanel() {
|
||||||
|
$(`#${PANEL_ID}`).remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncAlternatePresentCharactersTheme() {
|
||||||
|
const $panel = $(`#${PANEL_ID}`);
|
||||||
|
if (!$panel.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = extensionSettings.theme || 'default';
|
||||||
|
|
||||||
|
$panel.css({
|
||||||
|
'--rpg-bg': '',
|
||||||
|
'--rpg-accent': '',
|
||||||
|
'--rpg-text': '',
|
||||||
|
'--rpg-highlight': '',
|
||||||
|
'--rpg-border': '',
|
||||||
|
'--rpg-shadow': ''
|
||||||
|
});
|
||||||
|
|
||||||
|
if (theme === 'default') {
|
||||||
|
$panel.removeAttr('data-theme');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$panel.attr('data-theme', theme);
|
||||||
|
|
||||||
|
if (theme === 'custom') {
|
||||||
|
const colors = extensionSettings.customColors || {};
|
||||||
|
const bgColor = hexToRgba(colors.bg || '#1a1a2e', colors.bgOpacity ?? 100);
|
||||||
|
const accentColor = hexToRgba(colors.accent || '#16213e', colors.accentOpacity ?? 100);
|
||||||
|
const textColor = hexToRgba(colors.text || '#eaeaea', colors.textOpacity ?? 100);
|
||||||
|
const highlightColor = hexToRgba(colors.highlight || '#e94560', colors.highlightOpacity ?? 100);
|
||||||
|
const shadowColor = hexToRgba(colors.highlight || '#e94560', (colors.highlightOpacity ?? 100) * 0.5);
|
||||||
|
|
||||||
|
$panel.css({
|
||||||
|
'--rpg-bg': bgColor,
|
||||||
|
'--rpg-accent': accentColor,
|
||||||
|
'--rpg-text': textColor,
|
||||||
|
'--rpg-highlight': highlightColor,
|
||||||
|
'--rpg-border': highlightColor,
|
||||||
|
'--rpg-shadow': shadowColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderAlternatePresentCharacters({ useCommittedFallback = true } = {}) {
|
||||||
|
if (!extensionSettings.enabled || !extensionSettings.showAlternatePresentCharactersPanel) {
|
||||||
|
removeAlternatePresentCharactersPanel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const characterThoughtsData = getPresentCharactersTrackerData({ useCommittedFallback });
|
||||||
|
if (!characterThoughtsData) {
|
||||||
|
const $panel = ensureAlternatePresentCharactersPanel();
|
||||||
|
$panel.empty().hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentCharacters = parsePresentCharacters(characterThoughtsData);
|
||||||
|
if (presentCharacters.length === 0) {
|
||||||
|
const $panel = ensureAlternatePresentCharactersPanel();
|
||||||
|
$panel.empty().hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = i18n.getTranslation('template.trackerEditorModal.tabs.presentCharacters') || 'Present Characters';
|
||||||
|
|
||||||
|
const $panel = ensureAlternatePresentCharactersPanel();
|
||||||
|
const $header = $('<div class="rpg-alt-present-characters__header"></div>');
|
||||||
|
const $headerTitle = $('<div class="rpg-alt-present-characters__title"></div>');
|
||||||
|
const $scroll = $('<div class="rpg-alt-present-characters__scroll"></div>');
|
||||||
|
const $track = $('<div class="rpg-alt-present-characters__track"></div>');
|
||||||
|
|
||||||
|
$headerTitle.append(
|
||||||
|
$('<i class="fa-solid fa-users" aria-hidden="true"></i>'),
|
||||||
|
$('<span></span>').text(title)
|
||||||
|
);
|
||||||
|
|
||||||
|
$header.append(
|
||||||
|
$headerTitle,
|
||||||
|
$('<div class="rpg-alt-present-characters__count"></div>').text(String(presentCharacters.length))
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const character of presentCharacters) {
|
||||||
|
$track.append(createAlternatePresentCharacterCard(character));
|
||||||
|
}
|
||||||
|
|
||||||
|
$scroll.append($track);
|
||||||
|
|
||||||
|
$panel.empty().append($header, $scroll).show();
|
||||||
|
syncAlternatePresentCharactersTheme();
|
||||||
|
}
|
||||||
+297
-5
@@ -1,10 +1,277 @@
|
|||||||
/**
|
/**
|
||||||
* Desktop UI Module
|
* Desktop UI Module
|
||||||
* Handles desktop-specific UI functionality: tab navigation
|
* Handles desktop-specific UI functionality: tab navigation and strip widgets
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { extensionSettings } from '../../core/state.js';
|
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
||||||
|
import { hexToRgba } from './theme.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to parse time string and calculate clock hand angles
|
||||||
|
*/
|
||||||
|
function parseTimeForClock(timeStr) {
|
||||||
|
const timeMatch = timeStr.match(/(\d+):(\d+)/);
|
||||||
|
if (timeMatch) {
|
||||||
|
const hours = parseInt(timeMatch[1]);
|
||||||
|
const minutes = parseInt(timeMatch[2]);
|
||||||
|
const hourAngle = (hours % 12) * 30 + minutes * 0.5; // 30° per hour + 0.5° per minute
|
||||||
|
const minuteAngle = minutes * 6; // 6° per minute
|
||||||
|
return { hourAngle, minuteAngle };
|
||||||
|
}
|
||||||
|
return { hourAngle: 0, minuteAngle: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the desktop strip widgets display based on current tracker data and settings.
|
||||||
|
* Strip widgets are shown vertically in the collapsed panel strip.
|
||||||
|
*/
|
||||||
|
export function updateStripWidgets() {
|
||||||
|
const $panel = $('#rpg-companion-panel');
|
||||||
|
const $container = $('#rpg-strip-widget-container');
|
||||||
|
|
||||||
|
if ($panel.length === 0 || $container.length === 0) return;
|
||||||
|
|
||||||
|
// Check if strip widgets are enabled
|
||||||
|
const widgetSettings = extensionSettings.desktopStripWidgets;
|
||||||
|
if (!widgetSettings || !widgetSettings.enabled) {
|
||||||
|
$panel.removeClass('rpg-strip-widgets-enabled');
|
||||||
|
$container.find('.rpg-strip-widget').removeClass('rpg-strip-widget-visible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add enabled class to panel for CSS styling (wider collapsed width)
|
||||||
|
$panel.addClass('rpg-strip-widgets-enabled');
|
||||||
|
|
||||||
|
// Get tracker data - use imported state directly
|
||||||
|
const infoBox = lastGeneratedData?.infoBox || committedTrackerData?.infoBox;
|
||||||
|
|
||||||
|
// Parse infoBox if it's a string
|
||||||
|
let infoData = null;
|
||||||
|
if (infoBox) {
|
||||||
|
try {
|
||||||
|
infoData = typeof infoBox === 'string' ? JSON.parse(infoBox) : infoBox;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG Strip Widgets] Failed to parse infoBox:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather Icon Widget (with description)
|
||||||
|
const $weatherWidget = $container.find('.rpg-strip-widget-weather');
|
||||||
|
if (widgetSettings.weatherIcon?.enabled && infoData?.weather?.emoji) {
|
||||||
|
$weatherWidget.find('.rpg-strip-widget-icon').text(infoData.weather.emoji);
|
||||||
|
// Show weather description truncated
|
||||||
|
const forecast = infoData.weather.forecast || '';
|
||||||
|
const displayForecast = forecast.length > 12 ? forecast.substring(0, 10) + '…' : forecast;
|
||||||
|
$weatherWidget.find('.rpg-strip-widget-desc').text(displayForecast);
|
||||||
|
$weatherWidget.attr('title', forecast || 'Weather');
|
||||||
|
$weatherWidget.addClass('rpg-strip-widget-visible');
|
||||||
|
} else {
|
||||||
|
$weatherWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clock Widget with animated face
|
||||||
|
const $clockWidget = $container.find('.rpg-strip-widget-clock');
|
||||||
|
if (widgetSettings.clock?.enabled && infoData?.time) {
|
||||||
|
const timeStr = infoData.time.end || infoData.time.value || infoData.time.start || '';
|
||||||
|
if (timeStr) {
|
||||||
|
// Update clock hands
|
||||||
|
const { hourAngle, minuteAngle } = parseTimeForClock(timeStr);
|
||||||
|
$clockWidget.find('.rpg-strip-clock-hour').css('transform', `rotate(${hourAngle}deg)`);
|
||||||
|
$clockWidget.find('.rpg-strip-clock-minute').css('transform', `rotate(${minuteAngle}deg)`);
|
||||||
|
$clockWidget.find('.rpg-strip-widget-value').text(timeStr);
|
||||||
|
$clockWidget.attr('title', `Time: ${timeStr}`);
|
||||||
|
$clockWidget.addClass('rpg-strip-widget-visible');
|
||||||
|
} else {
|
||||||
|
$clockWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$clockWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date Widget
|
||||||
|
const $dateWidget = $container.find('.rpg-strip-widget-date');
|
||||||
|
if (widgetSettings.date?.enabled && infoData?.date?.value) {
|
||||||
|
const dateVal = infoData.date.value;
|
||||||
|
// Truncate long dates for display
|
||||||
|
const displayDate = dateVal.length > 20 ? dateVal.substring(0, 18) + '…' : dateVal;
|
||||||
|
$dateWidget.find('.rpg-strip-widget-value').text(displayDate);
|
||||||
|
$dateWidget.attr('title', dateVal);
|
||||||
|
$dateWidget.addClass('rpg-strip-widget-visible');
|
||||||
|
} else {
|
||||||
|
$dateWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location Widget
|
||||||
|
const $locationWidget = $container.find('.rpg-strip-widget-location');
|
||||||
|
if (widgetSettings.location?.enabled && infoData?.location?.value) {
|
||||||
|
const loc = infoData.location.value;
|
||||||
|
// Truncate long locations for display
|
||||||
|
const displayLoc = loc.length > 15 ? loc.substring(0, 13) + '…' : loc;
|
||||||
|
$locationWidget.find('.rpg-strip-widget-value').text(displayLoc);
|
||||||
|
$locationWidget.attr('title', loc);
|
||||||
|
$locationWidget.addClass('rpg-strip-widget-visible');
|
||||||
|
} else {
|
||||||
|
$locationWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats Widget - get from lastGeneratedData or committedTrackerData first, fallback to extensionSettings
|
||||||
|
const $statsWidget = $container.find('.rpg-strip-widget-stats');
|
||||||
|
if (widgetSettings.stats?.enabled) {
|
||||||
|
let allStats = [];
|
||||||
|
|
||||||
|
// Try to get stats from tracker data first (most current)
|
||||||
|
const userStatsData = lastGeneratedData?.userStats || committedTrackerData?.userStats;
|
||||||
|
if (userStatsData) {
|
||||||
|
try {
|
||||||
|
const parsedStats = typeof userStatsData === 'string' ? JSON.parse(userStatsData) : userStatsData;
|
||||||
|
if (parsedStats?.stats) {
|
||||||
|
allStats = parsedStats.stats;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG Strip Widgets] Failed to parse tracker userStats:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to extensionSettings.userStats
|
||||||
|
if (allStats.length === 0 && extensionSettings.userStats) {
|
||||||
|
try {
|
||||||
|
const userStatsJson = extensionSettings.userStats;
|
||||||
|
const parsedUserStats = typeof userStatsJson === 'string' ? JSON.parse(userStatsJson) : userStatsJson;
|
||||||
|
if (parsedUserStats?.stats) {
|
||||||
|
allStats = parsedUserStats.stats;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG Strip Widgets] Failed to parse extensionSettings.userStats:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allStats.length > 0) {
|
||||||
|
// Get enabled stats from trackerConfig
|
||||||
|
const configuredStats = extensionSettings.trackerConfig?.userStats?.customStats || [];
|
||||||
|
const enabledStatMap = new Map();
|
||||||
|
configuredStats.forEach(s => {
|
||||||
|
if (s.enabled !== false) {
|
||||||
|
enabledStatMap.set(s.id?.toLowerCase(), true);
|
||||||
|
enabledStatMap.set(s.name?.toLowerCase(), true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const $statsList = $statsWidget.find('.rpg-strip-stats-list');
|
||||||
|
$statsList.empty();
|
||||||
|
|
||||||
|
allStats.forEach(stat => {
|
||||||
|
// Filter by config if available - but if no config, show all
|
||||||
|
if (configuredStats.length > 0) {
|
||||||
|
const statId = stat.id?.toLowerCase();
|
||||||
|
const statName = stat.name?.toLowerCase();
|
||||||
|
if (!enabledStatMap.has(statId) && !enabledStatMap.has(statName)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = typeof stat.value === 'number' ? stat.value : parseInt(stat.value) || 0;
|
||||||
|
const color = getStatColor(value);
|
||||||
|
const abbr = stat.name.substring(0, 3).toUpperCase();
|
||||||
|
|
||||||
|
const $item = $(`<div class="rpg-strip-stat-item" title="${stat.name}: ${value}">
|
||||||
|
<span class="rpg-strip-stat-name">${abbr}</span>
|
||||||
|
<span class="rpg-strip-stat-value" style="color: ${color};">${value}</span>
|
||||||
|
</div>`);
|
||||||
|
$statsList.append($item);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($statsList.children().length > 0) {
|
||||||
|
$statsWidget.addClass('rpg-strip-widget-visible');
|
||||||
|
} else {
|
||||||
|
$statsWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$statsWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$statsWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attributes Widget
|
||||||
|
const $attrsWidget = $container.find('.rpg-strip-widget-attributes');
|
||||||
|
if (widgetSettings.attributes?.enabled) {
|
||||||
|
const showRPGAttributes = extensionSettings.trackerConfig?.userStats?.showRPGAttributes !== false;
|
||||||
|
|
||||||
|
if (showRPGAttributes && extensionSettings.classicStats) {
|
||||||
|
// Get enabled attributes from trackerConfig
|
||||||
|
const configuredAttrs = extensionSettings.trackerConfig?.userStats?.rpgAttributes || [];
|
||||||
|
const enabledAttrIds = configuredAttrs.filter(a => a.enabled !== false).map(a => a.id);
|
||||||
|
|
||||||
|
const attrs = extensionSettings.classicStats;
|
||||||
|
const $attrsGrid = $attrsWidget.find('.rpg-strip-attributes-grid');
|
||||||
|
$attrsGrid.empty();
|
||||||
|
|
||||||
|
Object.entries(attrs).forEach(([key, value]) => {
|
||||||
|
// Filter by config if available
|
||||||
|
if (enabledAttrIds.length > 0 && !enabledAttrIds.includes(key.toLowerCase())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $item = $(`<div class="rpg-strip-attr-item" title="${key.toUpperCase()}: ${value}">
|
||||||
|
<span class="rpg-strip-attr-name">${key.toUpperCase()}</span>
|
||||||
|
<span class="rpg-strip-attr-value">${value}</span>
|
||||||
|
</div>`);
|
||||||
|
$attrsGrid.append($item);
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($attrsGrid.children().length > 0) {
|
||||||
|
$attrsWidget.addClass('rpg-strip-widget-visible');
|
||||||
|
} else {
|
||||||
|
$attrsWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$attrsWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$attrsWidget.removeClass('rpg-strip-widget-visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a color interpolated between low and high based on stat value (0-100).
|
||||||
|
* @param {number} value - The stat value (0-100)
|
||||||
|
* @returns {string} CSS color value
|
||||||
|
*/
|
||||||
|
function getStatColor(value) {
|
||||||
|
const lowColor = extensionSettings.statBarColorLow || '#cc3333';
|
||||||
|
const lowOpacity = extensionSettings.statBarColorLowOpacity ?? 100;
|
||||||
|
const highColor = extensionSettings.statBarColorHigh || '#33cc66';
|
||||||
|
const highOpacity = extensionSettings.statBarColorHighOpacity ?? 100;
|
||||||
|
|
||||||
|
// Simple linear interpolation between low and high colors
|
||||||
|
const percent = Math.min(100, Math.max(0, value)) / 100;
|
||||||
|
|
||||||
|
// Parse colors
|
||||||
|
const lowRGB = hexToRgb(lowColor);
|
||||||
|
const highRGB = hexToRgb(highColor);
|
||||||
|
|
||||||
|
if (!lowRGB || !highRGB) return value > 50 ? hexToRgba(highColor, highOpacity) : hexToRgba(lowColor, lowOpacity);
|
||||||
|
|
||||||
|
const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent);
|
||||||
|
const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent);
|
||||||
|
const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent);
|
||||||
|
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
|
||||||
|
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a hex color to RGB object.
|
||||||
|
* @param {string} hex - Hex color string (e.g., "#cc3333")
|
||||||
|
* @returns {{r: number, g: number, b: number}|null}
|
||||||
|
*/
|
||||||
|
function hexToRgb(hex) {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
return result ? {
|
||||||
|
r: parseInt(result[1], 16),
|
||||||
|
g: parseInt(result[2], 16),
|
||||||
|
b: parseInt(result[3], 16)
|
||||||
|
} : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up desktop tab navigation for organizing content.
|
* Sets up desktop tab navigation for organizing content.
|
||||||
@@ -25,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)
|
||||||
@@ -55,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(`
|
||||||
@@ -70,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)
|
||||||
@@ -94,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)
|
||||||
@@ -111,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
|
||||||
@@ -143,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
|
||||||
@@ -152,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
|
||||||
@@ -169,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,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();
|
||||||
}
|
}
|
||||||
|
|||||||
+221
-135
@@ -4,7 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getContext } from '../../../../../../extensions.js';
|
import { getContext } from '../../../../../../extensions.js';
|
||||||
import { generateRaw, chat, saveChatDebounced, characters, this_chid, user_avatar } from '../../../../../../../script.js';
|
import { chat, saveChatDebounced, characters, this_chid, user_avatar } from '../../../../../../../script.js';
|
||||||
|
import { safeGenerateRaw } from '../../utils/responseExtractor.js';
|
||||||
import { selected_group, getGroupMembers, groups } from '../../../../../../group-chats.js';
|
import { selected_group, getGroupMembers, groups } from '../../../../../../group-chats.js';
|
||||||
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
|
import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js';
|
||||||
import { extensionSettings } from '../../core/state.js';
|
import { extensionSettings } from '../../core/state.js';
|
||||||
@@ -70,7 +71,7 @@ export class EncounterModal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show loading state
|
// Show loading state
|
||||||
this.showLoadingState('Initializing combat encounter...');
|
this.showLoadingState(i18n.getTranslation('encounter.ui.initializingCombatEncounter') || 'Initializing combat encounter...');
|
||||||
|
|
||||||
// Open the modal
|
// Open the modal
|
||||||
this.modal.classList.add('is-open');
|
this.modal.classList.add('is-open');
|
||||||
@@ -81,13 +82,13 @@ export class EncounterModal {
|
|||||||
// Store request for potential regeneration
|
// Store request for potential regeneration
|
||||||
this.lastRequest = { type: 'init', prompt: initPrompt };
|
this.lastRequest = { type: 'init', prompt: initPrompt };
|
||||||
|
|
||||||
const response = await generateRaw({
|
const response = await safeGenerateRaw({
|
||||||
prompt: initPrompt,
|
prompt: initPrompt,
|
||||||
quietToLoud: false
|
quietToLoud: false
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
this.showErrorWithRegenerate('No response received from AI. The model may be unavailable.');
|
this.showErrorWithRegenerate(i18n.getTranslation('encounter.ui.error.noResponse') || 'No response received from AI. The model may be unavailable.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ export class EncounterModal {
|
|||||||
const combatData = parseEncounterJSON(response);
|
const combatData = parseEncounterJSON(response);
|
||||||
|
|
||||||
if (!combatData || !combatData.party || !combatData.enemies) {
|
if (!combatData || !combatData.party || !combatData.enemies) {
|
||||||
this.showErrorWithRegenerate('Invalid JSON format detected. The AI returned malformed data. Ensure the Max Response Length is set to at least 2048 tokens, otherwise the model might run out of tokens and produce unfinished structures.');
|
this.showErrorWithRegenerate(i18n.getTranslation('encounter.ui.error.invalidJsonFormat') || 'Invalid JSON format detected. The AI returned malformed data. Ensure the Max Response Length is set to at least 2048 tokens, otherwise the model might run out of tokens and produce unfinished structures.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ export class EncounterModal {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] Error initializing encounter:', error);
|
console.error('[RPG Companion] Error initializing encounter:', error);
|
||||||
this.showErrorWithRegenerate(`Failed to initialize combat: ${error.message}`);
|
this.showErrorWithRegenerate(`${i18n.getTranslation('encounter.ui.error.failedToInitialize') || 'Failed to initialize combat:'} ${error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
this.isInitializing = false;
|
this.isInitializing = false;
|
||||||
}
|
}
|
||||||
@@ -141,94 +142,94 @@ export class EncounterModal {
|
|||||||
<div class="rpg-encounter-overlay"></div>
|
<div class="rpg-encounter-overlay"></div>
|
||||||
<div class="rpg-encounter-container" style="max-width: 600px;">
|
<div class="rpg-encounter-container" style="max-width: 600px;">
|
||||||
<div class="rpg-encounter-header">
|
<div class="rpg-encounter-header">
|
||||||
<h2><i class="fa-solid fa-book-open"></i> Configure Combat Narrative</h2>
|
<h2><i class="fa-solid fa-book-open"></i> ${i18n.getTranslation('encounter.configModal.title') || 'Configure Combat Narrative'}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-encounter-content" style="padding: 24px;">
|
<div class="rpg-encounter-content" style="padding: 24px;">
|
||||||
<div class="rpg-narrative-config-section">
|
<div class="rpg-narrative-config-section">
|
||||||
<label class="label_text" style="margin-bottom: 16px; display: block; font-weight: 600;">
|
<label class="label_text" style="margin-bottom: 16px; display: block; font-weight: 600;">
|
||||||
<i class="fa-solid fa-swords"></i> Combat Narrative Style
|
<i class="fa-solid fa-swords"></i> ${i18n.getTranslation('encounter.configModal.combatNarrativeStyle') || 'Combat Narrative Style'}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
||||||
<label for="config-combat-tense" style="min-width: 100px;">Tense:</label>
|
<label for="config-combat-tense" style="min-width: 100px;">${i18n.getTranslation('encounter.configModal.labels.tense') || 'Tense:'}</label>
|
||||||
<select id="config-combat-tense" class="rpg-select" style="flex: 1;">
|
<select id="config-combat-tense" class="rpg-select" style="flex: 1;">
|
||||||
<option value="present" ${combatDefaults.tense === 'present' ? 'selected' : ''}>Present</option>
|
<option value="present" ${combatDefaults.tense === 'present' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.present') || 'Present'}</option>
|
||||||
<option value="past" ${combatDefaults.tense === 'past' ? 'selected' : ''}>Past</option>
|
<option value="past" ${combatDefaults.tense === 'past' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.past') || 'Past'}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
||||||
<label for="config-combat-person" style="min-width: 100px;">Person:</label>
|
<label for="config-combat-person" style="min-width: 100px;">${i18n.getTranslation('encounter.configModal.labels.person') || 'Person:'}</label>
|
||||||
<select id="config-combat-person" class="rpg-select" style="flex: 1;">
|
<select id="config-combat-person" class="rpg-select" style="flex: 1;">
|
||||||
<option value="first" ${combatDefaults.person === 'first' ? 'selected' : ''}>First Person</option>
|
<option value="first" ${combatDefaults.person === 'first' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.firstPerson') || 'First Person'}</option>
|
||||||
<option value="second" ${combatDefaults.person === 'second' ? 'selected' : ''}>Second Person</option>
|
<option value="second" ${combatDefaults.person === 'second' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.secondPerson') || 'Second Person'}</option>
|
||||||
<option value="third" ${combatDefaults.person === 'third' ? 'selected' : ''}>Third Person</option>
|
<option value="third" ${combatDefaults.person === 'third' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.thirdPerson') || 'Third Person'}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
||||||
<label for="config-combat-narration" style="min-width: 100px;">Narration:</label>
|
<label for="config-combat-narration" style="min-width: 100px;">${i18n.getTranslation('encounter.configModal.labels.narration') || 'Narration:'}</label>
|
||||||
<select id="config-combat-narration" class="rpg-select" style="flex: 1;">
|
<select id="config-combat-narration" class="rpg-select" style="flex: 1;">
|
||||||
<option value="omniscient" ${combatDefaults.narration === 'omniscient' ? 'selected' : ''}>Omniscient</option>
|
<option value="omniscient" ${combatDefaults.narration === 'omniscient' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.omniscient') || 'Omniscient'}</option>
|
||||||
<option value="limited" ${combatDefaults.narration === 'limited' ? 'selected' : ''}>Limited</option>
|
<option value="limited" ${combatDefaults.narration === 'limited' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.limited') || 'Limited'}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
||||||
<label for="config-combat-pov" style="min-width: 100px;">Point of View:</label>
|
<label for="config-combat-pov" style="min-width: 100px;">${i18n.getTranslation('encounter.configModal.labels.pointOfView') || 'Point of View:'}</label>
|
||||||
<input type="text" id="config-combat-pov" class="text_pole" placeholder="narrator" value="${combatDefaults.pov || ''}" style="flex: 1;" />
|
<input type="text" id="config-combat-pov" class="text_pole" placeholder="${i18n.getTranslation('encounter.configModal.placeholders.narrator') || 'narrator'}" value="${combatDefaults.pov || ''}" style="flex: 1;" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rpg-narrative-config-section" style="margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--rpg-border, rgba(255,255,255,0.1));">
|
<div class="rpg-narrative-config-section" style="margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--rpg-border, rgba(255,255,255,0.1));">
|
||||||
<label class="label_text" style="margin-bottom: 16px; display: block; font-weight: 600;">
|
<label class="label_text" style="margin-bottom: 16px; display: block; font-weight: 600;">
|
||||||
<i class="fa-solid fa-scroll"></i> Combat Summary Style
|
<i class="fa-solid fa-scroll"></i> ${i18n.getTranslation('encounter.configModal.combatSummaryStyle') || 'Combat Summary Style'}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
||||||
<label for="config-summary-tense" style="min-width: 100px;">Tense:</label>
|
<label for="config-summary-tense" style="min-width: 100px;">${i18n.getTranslation('encounter.configModal.labels.tense') || 'Tense:'}</label>
|
||||||
<select id="config-summary-tense" class="rpg-select" style="flex: 1;">
|
<select id="config-summary-tense" class="rpg-select" style="flex: 1;">
|
||||||
<option value="present" ${summaryDefaults.tense === 'present' ? 'selected' : ''}>Present</option>
|
<option value="present" ${summaryDefaults.tense === 'present' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.present') || 'Present'}</option>
|
||||||
<option value="past" ${summaryDefaults.tense === 'past' ? 'selected' : ''}>Past</option>
|
<option value="past" ${summaryDefaults.tense === 'past' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.past') || 'Past'}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
||||||
<label for="config-summary-person" style="min-width: 100px;">Person:</label>
|
<label for="config-summary-person" style="min-width: 100px;">${i18n.getTranslation('encounter.configModal.labels.person') || 'Person:'}</label>
|
||||||
<select id="config-summary-person" class="rpg-select" style="flex: 1;">
|
<select id="config-summary-person" class="rpg-select" style="flex: 1;">
|
||||||
<option value="first" ${summaryDefaults.person === 'first' ? 'selected' : ''}>First Person</option>
|
<option value="first" ${summaryDefaults.person === 'first' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.firstPerson') || 'First Person'}</option>
|
||||||
<option value="second" ${summaryDefaults.person === 'second' ? 'selected' : ''}>Second Person</option>
|
<option value="second" ${summaryDefaults.person === 'second' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.secondPerson') || 'Second Person'}</option>
|
||||||
<option value="third" ${summaryDefaults.person === 'third' ? 'selected' : ''}>Third Person</option>
|
<option value="third" ${summaryDefaults.person === 'third' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.thirdPerson') || 'Third Person'}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
||||||
<label for="config-summary-narration" style="min-width: 100px;">Narration:</label>
|
<label for="config-summary-narration" style="min-width: 100px;">${i18n.getTranslation('encounter.configModal.labels.narration') || 'Narration:'}</label>
|
||||||
<select id="config-summary-narration" class="rpg-select" style="flex: 1;">
|
<select id="config-summary-narration" class="rpg-select" style="flex: 1;">
|
||||||
<option value="omniscient" ${summaryDefaults.narration === 'omniscient' ? 'selected' : ''}>Omniscient</option>
|
<option value="omniscient" ${summaryDefaults.narration === 'omniscient' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.omniscient') || 'Omniscient'}</option>
|
||||||
<option value="limited" ${summaryDefaults.narration === 'limited' ? 'selected' : ''}>Limited</option>
|
<option value="limited" ${summaryDefaults.narration === 'limited' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.limited') || 'Limited'}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
<div class="rpg-setting-row" style="margin-bottom: 12px;">
|
||||||
<label for="config-summary-pov" style="min-width: 100px;">Point of View:</label>
|
<label for="config-summary-pov" style="min-width: 100px;">${i18n.getTranslation('encounter.configModal.labels.pointOfView') || 'Point of View:'}</label>
|
||||||
<input type="text" id="config-summary-pov" class="text_pole" placeholder="narrator" value="${summaryDefaults.pov || ''}" style="flex: 1;" />
|
<input type="text" id="config-summary-pov" class="text_pole" placeholder="${i18n.getTranslation('encounter.configModal.placeholders.narrator') || 'narrator'}" value="${summaryDefaults.pov || ''}" style="flex: 1;" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--rpg-border, rgba(255,255,255,0.1));">
|
<div style="margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--rpg-border, rgba(255,255,255,0.1));">
|
||||||
<label class="checkbox_label" style="display: flex; align-items: center; gap: 8px;">
|
<label class="checkbox_label" style="display: flex; align-items: center; gap: 8px;">
|
||||||
<input type="checkbox" id="config-remember" ${extensionSettings.encounterSettings?.narrativeConfigured ? 'checked' : ''} style="margin: 0;" />
|
<input type="checkbox" id="config-remember" ${extensionSettings.encounterSettings?.narrativeConfigured ? 'checked' : ''} style="margin: 0;" />
|
||||||
<span style="color: var(--rpg-text, #eaeaea);">Remember these settings for future encounters</span>
|
<span style="color: var(--rpg-text, #eaeaea);">${i18n.getTranslation('encounter.configModal.rememberSettings') || 'Remember these settings for future encounters'}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 24px; display: flex; gap: 12px; justify-content: flex-end;">
|
<div style="margin-top: 24px; display: flex; gap: 12px; justify-content: flex-end;">
|
||||||
<button id="config-cancel" class="rpg-btn rpg-btn-secondary" style="padding: 12px 24px;">
|
<button id="config-cancel" class="rpg-btn rpg-btn-secondary" style="padding: 12px 24px;">
|
||||||
<i class="fa-solid fa-times"></i> Cancel
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
|
||||||
</button>
|
</button>
|
||||||
<button id="config-proceed" class="rpg-btn rpg-btn-primary" style="padding: 12px 24px;">
|
<button id="config-proceed" class="rpg-btn rpg-btn-primary" style="padding: 12px 24px;">
|
||||||
<i class="fa-solid fa-play"></i> Proceed
|
<i class="fa-solid fa-play"></i> ${i18n.getTranslation('encounter.configModal.buttons.proceed') || 'Proceed'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -302,12 +303,12 @@ export class EncounterModal {
|
|||||||
<div class="rpg-encounter-overlay"></div>
|
<div class="rpg-encounter-overlay"></div>
|
||||||
<div class="rpg-encounter-container">
|
<div class="rpg-encounter-container">
|
||||||
<div class="rpg-encounter-header">
|
<div class="rpg-encounter-header">
|
||||||
<h2><i class="fa-solid fa-swords"></i> Combat Encounter</h2>
|
<h2><i class="fa-solid fa-swords"></i> ${i18n.getTranslation('encounter.ui.combatEncounterTitle') || 'Combat Encounter'}</h2>
|
||||||
<div class="rpg-encounter-header-buttons">
|
<div class="rpg-encounter-header-buttons">
|
||||||
<button id="rpg-encounter-conclude" class="rpg-encounter-conclude-btn" title="Conclude encounter early">
|
<button id="rpg-encounter-conclude" class="rpg-encounter-conclude-btn" title="${i18n.getTranslation('encounter.ui.concludeEncounterTitle') || 'Conclude encounter early'}">
|
||||||
<i class="fa-solid fa-flag-checkered"></i> Conclude Encounter
|
<i class="fa-solid fa-flag-checkered"></i> ${i18n.getTranslation('encounter.ui.concludeEncounterButton') || 'Conclude Encounter'}
|
||||||
</button>
|
</button>
|
||||||
<button id="rpg-encounter-close" class="rpg-encounter-close-btn" title="Close (ends combat)">
|
<button id="rpg-encounter-close" class="rpg-encounter-close-btn" title="${i18n.getTranslation('encounter.ui.closeTitle') || 'Close (ends combat)'}">
|
||||||
<i class="fa-solid fa-times"></i>
|
<i class="fa-solid fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -315,7 +316,7 @@ export class EncounterModal {
|
|||||||
<div class="rpg-encounter-content">
|
<div class="rpg-encounter-content">
|
||||||
<div id="rpg-encounter-loading" class="rpg-encounter-loading">
|
<div id="rpg-encounter-loading" class="rpg-encounter-loading">
|
||||||
<i class="fa-solid fa-spinner fa-spin"></i>
|
<i class="fa-solid fa-spinner fa-spin"></i>
|
||||||
<p>Initializing combat...</p>
|
<p>${i18n.getTranslation('encounter.ui.initializingCombat') || 'Initializing combat...'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="rpg-encounter-main" class="rpg-encounter-main" style="display: none;">
|
<div id="rpg-encounter-main" class="rpg-encounter-main" style="display: none;">
|
||||||
<!-- Combat UI will be rendered here -->
|
<!-- Combat UI will be rendered here -->
|
||||||
@@ -330,20 +331,20 @@ export class EncounterModal {
|
|||||||
|
|
||||||
// Add event listeners
|
// Add event listeners
|
||||||
this.modal.querySelector('#rpg-encounter-conclude').addEventListener('click', () => {
|
this.modal.querySelector('#rpg-encounter-conclude').addEventListener('click', () => {
|
||||||
if (confirm('Conclude this encounter early and generate a summary?')) {
|
if (confirm(i18n.getTranslation('encounter.ui.confirmConcludeEarly') || 'Conclude this encounter early and generate a summary?')) {
|
||||||
this.concludeEncounter();
|
this.concludeEncounter();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.modal.querySelector('#rpg-encounter-close').addEventListener('click', () => {
|
this.modal.querySelector('#rpg-encounter-close').addEventListener('click', () => {
|
||||||
if (confirm('Are you sure you want to end this combat encounter?')) {
|
if (confirm(i18n.getTranslation('encounter.ui.confirmEndCombat') || 'Are you sure you want to end this combat encounter?')) {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close on overlay click
|
// Close on overlay click
|
||||||
this.modal.querySelector('.rpg-encounter-overlay').addEventListener('click', () => {
|
this.modal.querySelector('.rpg-encounter-overlay').addEventListener('click', () => {
|
||||||
if (confirm('Are you sure you want to end this combat encounter?')) {
|
if (confirm(i18n.getTranslation('encounter.ui.confirmEndCombat') || 'Are you sure you want to end this combat encounter?')) {
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -367,12 +368,12 @@ export class EncounterModal {
|
|||||||
<div class="rpg-encounter-battlefield">
|
<div class="rpg-encounter-battlefield">
|
||||||
<!-- Environment -->
|
<!-- Environment -->
|
||||||
<div class="rpg-encounter-environment">
|
<div class="rpg-encounter-environment">
|
||||||
<p><i class="fa-solid fa-mountain"></i> ${combatData.environment || 'Battle Arena'}</p>
|
<p><i class="fa-solid fa-mountain"></i> ${combatData.environment || i18n.getTranslation('encounter.ui.environment.default') || 'Battle Arena'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Enemies Section -->
|
<!-- Enemies Section -->
|
||||||
<div class="rpg-encounter-section">
|
<div class="rpg-encounter-section">
|
||||||
<h3><i class="fa-solid fa-skull"></i> Enemies</h3>
|
<h3><i class="fa-solid fa-skull"></i> ${i18n.getTranslation('encounter.ui.enemiesTitle') || 'Enemies'}</h3>
|
||||||
<div class="rpg-encounter-enemies">
|
<div class="rpg-encounter-enemies">
|
||||||
${this.renderEnemies(combatData.enemies)}
|
${this.renderEnemies(combatData.enemies)}
|
||||||
</div>
|
</div>
|
||||||
@@ -380,7 +381,7 @@ export class EncounterModal {
|
|||||||
|
|
||||||
<!-- Party Section -->
|
<!-- Party Section -->
|
||||||
<div class="rpg-encounter-section">
|
<div class="rpg-encounter-section">
|
||||||
<h3><i class="fa-solid fa-users"></i> Party</h3>
|
<h3><i class="fa-solid fa-users"></i> ${i18n.getTranslation('encounter.ui.partyTitle') || 'Party'}</h3>
|
||||||
<div class="rpg-encounter-party">
|
<div class="rpg-encounter-party">
|
||||||
${this.renderParty(combatData.party)}
|
${this.renderParty(combatData.party)}
|
||||||
</div>
|
</div>
|
||||||
@@ -388,16 +389,16 @@ export class EncounterModal {
|
|||||||
|
|
||||||
<!-- Combat Log -->
|
<!-- Combat Log -->
|
||||||
<div class="rpg-encounter-log-section">
|
<div class="rpg-encounter-log-section">
|
||||||
<h3><i class="fa-solid fa-scroll"></i> Combat Log</h3>
|
<h3><i class="fa-solid fa-scroll"></i> ${i18n.getTranslation('encounter.ui.combatLog') || 'Combat Log'}</h3>
|
||||||
<div id="rpg-encounter-log" class="rpg-encounter-log">
|
<div id="rpg-encounter-log" class="rpg-encounter-log">
|
||||||
<div class="rpg-encounter-log-entry">
|
<div class="rpg-encounter-log-entry">
|
||||||
<em>Combat begins!</em>
|
<em>${i18n.getTranslation('encounter.ui.combatBegins') || 'Combat begins!'}</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Player Controls -->
|
<!-- Player Controls -->
|
||||||
${this.renderPlayerControls(combatData.party)}
|
${this.renderPlayerControls(combatData.party, currentEncounter.playerActions)}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -419,7 +420,7 @@ export class EncounterModal {
|
|||||||
|
|
||||||
// Try to find avatar for enemy (they might be a character from the chat or Present Characters)
|
// Try to find avatar for enemy (they might be a character from the chat or Present Characters)
|
||||||
const avatarUrl = this.getCharacterAvatar(enemy.name);
|
const avatarUrl = this.getCharacterAvatar(enemy.name);
|
||||||
const sprite = enemy.sprite || '👹';
|
const sprite = enemy.sprite || i18n.getTranslation('encounter.ui.enemyDefaultEmoji') || '👹';
|
||||||
|
|
||||||
// Fallback SVG if no avatar found
|
// Fallback SVG if no avatar found
|
||||||
const fallbackSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2NjY2NjYyIgb3BhY2l0eT0iMC4zIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2IiBmb250LXNpemU9IjQwIj4/PC90ZXh0Pjwvc3ZnPg==';
|
const fallbackSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2NjY2NjYyIgb3BhY2l0eT0iMC4zIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2IiBmb250LXNpemU9IjQwIj4/PC90ZXh0Pjwvc3ZnPg==';
|
||||||
@@ -433,7 +434,7 @@ export class EncounterModal {
|
|||||||
<h4>${enemy.name}</h4>
|
<h4>${enemy.name}</h4>
|
||||||
<div class="rpg-encounter-hp-bar">
|
<div class="rpg-encounter-hp-bar">
|
||||||
<div class="rpg-encounter-hp-fill" style="width: ${hpPercent}%"></div>
|
<div class="rpg-encounter-hp-fill" style="width: ${hpPercent}%"></div>
|
||||||
<span class="rpg-encounter-hp-text">${enemy.hp}/${enemy.maxHp} HP</span>
|
<span class="rpg-encounter-hp-text">${enemy.hp}/${enemy.maxHp}${i18n.getTranslation('encounter.ui.hpSuffix') || ' HP'}</span>
|
||||||
</div>
|
</div>
|
||||||
${enemy.statuses && enemy.statuses.length > 0 ? `
|
${enemy.statuses && enemy.statuses.length > 0 ? `
|
||||||
<div class="rpg-encounter-statuses">
|
<div class="rpg-encounter-statuses">
|
||||||
@@ -480,10 +481,10 @@ export class EncounterModal {
|
|||||||
<img src="${avatarUrl || fallbackSvg}" alt="${member.name}" onerror="this.src='${fallbackSvg}'">
|
<img src="${avatarUrl || fallbackSvg}" alt="${member.name}" onerror="this.src='${fallbackSvg}'">
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-encounter-card-info">
|
<div class="rpg-encounter-card-info">
|
||||||
<h4>${member.name} ${member.isPlayer ? '(You)' : ''}</h4>
|
<h4>${member.name} ${member.isPlayer ? i18n.getTranslation('encounter.ui.playerSuffix') || '(You)' : ''}</h4>
|
||||||
<div class="rpg-encounter-hp-bar">
|
<div class="rpg-encounter-hp-bar">
|
||||||
<div class="rpg-encounter-hp-fill rpg-encounter-hp-party" style="width: ${hpPercent}%"></div>
|
<div class="rpg-encounter-hp-fill rpg-encounter-hp-party" style="width: ${hpPercent}%"></div>
|
||||||
<span class="rpg-encounter-hp-text">${member.hp}/${member.maxHp} HP</span>
|
<span class="rpg-encounter-hp-text">${member.hp}/${member.maxHp}${i18n.getTranslation('encounter.ui.hpSuffix') || ' HP'}</span>
|
||||||
</div> ${member.statuses && member.statuses.length > 0 ? `
|
</div> ${member.statuses && member.statuses.length > 0 ? `
|
||||||
<div class="rpg-encounter-statuses">
|
<div class="rpg-encounter-statuses">
|
||||||
${member.statuses.map(status => `<span class="rpg-encounter-status" title="${status.name}">${status.emoji}</span>`).join('')}
|
${member.statuses.map(status => `<span class="rpg-encounter-status" title="${status.name}">${status.emoji}</span>`).join('')}
|
||||||
@@ -560,18 +561,18 @@ export class EncounterModal {
|
|||||||
targetOptions = `
|
targetOptions = `
|
||||||
<div class="rpg-target-option" data-target="all-enemies">
|
<div class="rpg-target-option" data-target="all-enemies">
|
||||||
<div class="rpg-target-icon">💥</div>
|
<div class="rpg-target-icon">💥</div>
|
||||||
<div class="rpg-target-name">All Enemies</div>
|
<div class="rpg-target-name">${i18n.getTranslation('encounter.ui.allEnemies') || 'All Enemies'}</div>
|
||||||
<div class="rpg-target-desc">Area of Effect</div>
|
<div class="rpg-target-desc">${i18n.getTranslation('encounter.ui.areaOfEffect') || 'Area of Effect'}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (attackType === 'both') {
|
} else if (attackType === 'both') {
|
||||||
targetOptions = `
|
targetOptions = `
|
||||||
<div class="rpg-target-option" data-target="all-enemies">
|
<div class="rpg-target-option" data-target="all-enemies">
|
||||||
<div class="rpg-target-icon">💥</div>
|
<div class="rpg-target-icon">💥</div>
|
||||||
<div class="rpg-target-name">All Enemies</div>
|
<div class="rpg-target-name">${i18n.getTranslation('encounter.ui.allEnemies') || 'All Enemies'}</div>
|
||||||
<div class="rpg-target-desc">Area of Effect</div>
|
<div class="rpg-target-desc">${i18n.getTranslation('encounter.ui.areaOfEffect') || 'Area of Effect'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-target-divider">OR</div>
|
<div class="rpg-target-divider">${i18n.getTranslation('encounter.ui.or') || 'OR'}</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,7 +585,7 @@ export class EncounterModal {
|
|||||||
<div class="rpg-target-option" data-target="${enemy.name}" data-target-type="enemy" data-target-index="${index}">
|
<div class="rpg-target-option" data-target="${enemy.name}" data-target-type="enemy" data-target-index="${index}">
|
||||||
<div class="rpg-target-icon">${enemy.sprite || '👹'}</div>
|
<div class="rpg-target-icon">${enemy.sprite || '👹'}</div>
|
||||||
<div class="rpg-target-name">${enemy.name}</div>
|
<div class="rpg-target-name">${enemy.name}</div>
|
||||||
<div class="rpg-target-hp">${enemy.hp}/${enemy.maxHp} HP</div>
|
<div class="rpg-target-hp">${enemy.hp}/${enemy.maxHp}${i18n.getTranslation('encounter.ui.hpSuffix') || ' HP'}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -593,13 +594,13 @@ export class EncounterModal {
|
|||||||
// Add party members (for heals/buffs)
|
// Add party members (for heals/buffs)
|
||||||
combatStats.party.forEach((member, index) => {
|
combatStats.party.forEach((member, index) => {
|
||||||
if (member.hp > 0) {
|
if (member.hp > 0) {
|
||||||
const isPlayer = member.isPlayer ? ' (You)' : '';
|
const isPlayer = member.isPlayer ? i18n.getTranslation('encounter.ui.playerSuffix') || ' (You)' : '';
|
||||||
// Get avatar for party member
|
// Get avatar for party member
|
||||||
let avatarIcon = '✨';
|
let avatarIcon = '✨';
|
||||||
if (member.isPlayer && user_avatar) {
|
if (member.isPlayer && user_avatar) {
|
||||||
avatarIcon = `<img src="${getSafeThumbnailUrl('persona', user_avatar)}" alt="${member.name}" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover;">`;
|
avatarIcon = `<img src="${getSafeThumbnailUrl('persona', user_avatar)}" alt="${member.name}" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover;">`;
|
||||||
} else {
|
} else {
|
||||||
const avatarUrl = this.getPartyMemberAvatar(member.name);
|
const avatarUrl = this.getCharacterAvatar(member.name);
|
||||||
if (avatarUrl) {
|
if (avatarUrl) {
|
||||||
avatarIcon = `<img src="${avatarUrl}" alt="${member.name}" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover;">`;
|
avatarIcon = `<img src="${avatarUrl}" alt="${member.name}" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover;">`;
|
||||||
}
|
}
|
||||||
@@ -608,7 +609,7 @@ export class EncounterModal {
|
|||||||
<div class="rpg-target-option rpg-target-ally" data-target="${member.name}" data-target-type="party" data-target-index="${index}">
|
<div class="rpg-target-option rpg-target-ally" data-target="${member.name}" data-target-type="party" data-target-index="${index}">
|
||||||
<div class="rpg-target-icon">${avatarIcon}</div>
|
<div class="rpg-target-icon">${avatarIcon}</div>
|
||||||
<div class="rpg-target-name">${member.name}${isPlayer}</div>
|
<div class="rpg-target-name">${member.name}${isPlayer}</div>
|
||||||
<div class="rpg-target-hp">${member.hp}/${member.maxHp} HP</div>
|
<div class="rpg-target-hp">${member.hp}/${member.maxHp}${i18n.getTranslation('encounter.ui.hpSuffix') || ' HP'}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -617,11 +618,11 @@ export class EncounterModal {
|
|||||||
|
|
||||||
targetModal.innerHTML = `
|
targetModal.innerHTML = `
|
||||||
<div class="rpg-target-selection-modal">
|
<div class="rpg-target-selection-modal">
|
||||||
<h3><i class="fa-solid fa-crosshairs"></i> Select Target</h3>
|
<h3><i class="fa-solid fa-crosshairs"></i> ${i18n.getTranslation('encounter.ui.selectTarget') || 'Select Target'}</h3>
|
||||||
<div class="rpg-target-list">
|
<div class="rpg-target-list">
|
||||||
${targetOptions}
|
${targetOptions}
|
||||||
</div>
|
</div>
|
||||||
<button class="rpg-target-cancel">Cancel</button>
|
<button class="rpg-target-cancel">${i18n.getTranslation('global.cancel') || 'Cancel'}</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -657,20 +658,24 @@ export class EncounterModal {
|
|||||||
* @param {Array} party - Party data
|
* @param {Array} party - Party data
|
||||||
* @returns {string} HTML for controls
|
* @returns {string} HTML for controls
|
||||||
*/
|
*/
|
||||||
renderPlayerControls(party) {
|
renderPlayerControls(party, playerActions = null) {
|
||||||
const player = party.find(m => m.isPlayer);
|
const player = party.find(m => m.isPlayer);
|
||||||
if (!player || player.hp <= 0) {
|
if (!player || player.hp <= 0) {
|
||||||
return '<div class="rpg-encounter-controls"><p class="rpg-encounter-defeated">You have been defeated...</p></div>';
|
return '<div class="rpg-encounter-controls"><p class="rpg-encounter-defeated">' + (i18n.getTranslation('encounter.ui.youHaveBeenDefeated') || 'You have been defeated...') + '</p></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use playerActions if provided, otherwise fall back to player data
|
||||||
|
const attacks = playerActions?.attacks || player.attacks || [];
|
||||||
|
const items = playerActions?.items || player.items || [];
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="rpg-encounter-controls">
|
<div class="rpg-encounter-controls">
|
||||||
<h3><i class="fa-solid fa-hand-fist"></i> Your Actions</h3>
|
<h3><i class="fa-solid fa-hand-fist"></i> ${i18n.getTranslation('encounter.ui.yourActions') || 'Your Actions'}</h3>
|
||||||
|
|
||||||
<div class="rpg-encounter-action-buttons">
|
<div class="rpg-encounter-action-buttons">
|
||||||
<div class="rpg-encounter-button-group">
|
<div class="rpg-encounter-button-group">
|
||||||
<h4>Attacks</h4>
|
<h4>${i18n.getTranslation('encounter.ui.attacks') || 'Attacks'}</h4>
|
||||||
${player.attacks.map(attack => {
|
${attacks.map(attack => {
|
||||||
// Support both old string format and new object format
|
// Support both old string format and new object format
|
||||||
const attackName = typeof attack === 'string' ? attack : attack.name;
|
const attackName = typeof attack === 'string' ? attack : attack.name;
|
||||||
const attackType = typeof attack === 'string' ? 'single-target' : (attack.type || 'single-target');
|
const attackType = typeof attack === 'string' ? 'single-target' : (attack.type || 'single-target');
|
||||||
@@ -681,17 +686,17 @@ export class EncounterModal {
|
|||||||
data-action="attack"
|
data-action="attack"
|
||||||
data-value="${attackName}"
|
data-value="${attackName}"
|
||||||
data-attack-type="${attackType}"
|
data-attack-type="${attackType}"
|
||||||
title="${attackType === 'AoE' ? 'Area of Effect' : attackType === 'both' ? 'Single or AoE' : 'Single Target'}">
|
title="${attackType === 'AoE' ? i18n.getTranslation('encounter.ui.attackType.aoe') || 'Area of Effect' : attackType === 'both' ? i18n.getTranslation('encounter.ui.attackType.both') || 'Single or AoE' : i18n.getTranslation('encounter.ui.attackType.single') || 'Single Target'}">
|
||||||
<i class="fa-solid fa-sword"></i> ${attackName} ${typeIcon}
|
<i class="fa-solid fa-sword"></i> ${attackName} ${typeIcon}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${player.items && player.items.length > 0 ? `
|
${items && items.length > 0 ? `
|
||||||
<div class="rpg-encounter-button-group">
|
<div class="rpg-encounter-button-group">
|
||||||
<h4>Items</h4>
|
<h4>${i18n.getTranslation('encounter.ui.items') || 'Items'}</h4>
|
||||||
${player.items.map(item => `
|
${items.map(item => `
|
||||||
<button class="rpg-encounter-action-btn rpg-encounter-item-btn" data-action="item" data-value="${item}">
|
<button class="rpg-encounter-action-btn rpg-encounter-item-btn" data-action="item" data-value="${item}">
|
||||||
<i class="fa-solid fa-flask"></i> ${item}
|
<i class="fa-solid fa-flask"></i> ${item}
|
||||||
</button>
|
</button>
|
||||||
@@ -701,11 +706,11 @@ export class EncounterModal {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rpg-encounter-custom-action">
|
<div class="rpg-encounter-custom-action">
|
||||||
<h4>Custom Action</h4>
|
<h4>${i18n.getTranslation('encounter.ui.customAction') || 'Custom Action'}</h4>
|
||||||
<div class="rpg-encounter-input-group">
|
<div class="rpg-encounter-input-group">
|
||||||
<input type="text" id="rpg-encounter-custom-input" placeholder="Describe what you want to do..." />
|
<input type="text" id="rpg-encounter-custom-input" placeholder="${i18n.getTranslation('encounter.ui.customActionPlaceholder') || 'Describe what you want to do...'}" />
|
||||||
<button id="rpg-encounter-custom-submit" class="rpg-encounter-submit-btn">
|
<button id="rpg-encounter-custom-submit" class="rpg-encounter-submit-btn">
|
||||||
<i class="fa-solid fa-paper-plane"></i> Submit
|
<i class="fa-solid fa-paper-plane"></i> ${i18n.getTranslation('encounter.ui.submit') || 'Submit'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -718,62 +723,74 @@ export class EncounterModal {
|
|||||||
* @param {Array} party - Party data for reference
|
* @param {Array} party - Party data for reference
|
||||||
*/
|
*/
|
||||||
attachControlListeners(party) {
|
attachControlListeners(party) {
|
||||||
// Attack and item buttons
|
// Only attach once - event delegation on the modal means listeners persist
|
||||||
this.modal.querySelectorAll('.rpg-encounter-action-btn').forEach(btn => {
|
if (this._listenersAttached) {
|
||||||
btn.addEventListener('click', async (e) => {
|
return;
|
||||||
const actionType = e.currentTarget.dataset.action;
|
}
|
||||||
const value = e.currentTarget.dataset.value;
|
|
||||||
const attackType = e.currentTarget.dataset.attackType;
|
// Store handlers as instance properties so we can remove them if needed
|
||||||
|
this._actionHandler = async (e) => {
|
||||||
|
// Handle action buttons (attack/item)
|
||||||
|
const actionBtn = e.target.closest('.rpg-encounter-action-btn');
|
||||||
|
if (actionBtn && !actionBtn.disabled && !this.isProcessing) {
|
||||||
|
const actionType = actionBtn.dataset.action;
|
||||||
|
const value = actionBtn.dataset.value;
|
||||||
|
const attackType = actionBtn.dataset.attackType;
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const userName = context.name1;
|
const userName = context.name1;
|
||||||
|
|
||||||
let actionText = '';
|
let actionText = '';
|
||||||
|
|
||||||
if (actionType === 'attack') {
|
if (actionType === 'attack') {
|
||||||
// Show target selection for attacks
|
|
||||||
const target = await this.showTargetSelection(attackType, currentEncounter.combatStats);
|
const target = await this.showTargetSelection(attackType, currentEncounter.combatStats);
|
||||||
if (!target) return; // User cancelled
|
if (!target) return;
|
||||||
|
|
||||||
if (target === 'all-enemies') {
|
if (target === 'all-enemies') {
|
||||||
actionText = `${userName} uses ${value} targeting all enemies!`;
|
actionText = `${userName} uses ${value}${i18n.getTranslation('encounter.ui.targetingAllEnemies') || ' targeting all enemies!'}`;
|
||||||
} else {
|
} else {
|
||||||
actionText = `${userName} uses ${value} on ${target}!`;
|
actionText = `${userName} uses ${value}${i18n.getTranslation('encounter.ui.on') || ' on '}${target}!`;
|
||||||
}
|
}
|
||||||
} else if (actionType === 'item') {
|
} else if (actionType === 'item') {
|
||||||
// Show target selection for items (default to single-target)
|
|
||||||
const target = await this.showTargetSelection('single-target', currentEncounter.combatStats);
|
const target = await this.showTargetSelection('single-target', currentEncounter.combatStats);
|
||||||
if (!target) return; // User cancelled
|
if (!target) return;
|
||||||
|
|
||||||
actionText = `${userName} uses ${value} on ${target}!`;
|
actionText = `${userName} uses ${value}${i18n.getTranslation('encounter.ui.on') || ' on '}${target}!`;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.processCombatAction(actionText);
|
await this.processCombatAction(actionText);
|
||||||
});
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
// Custom action submit
|
// Handle custom submit button
|
||||||
const customInput = this.modal.querySelector('#rpg-encounter-custom-input');
|
const submitBtn = e.target.closest('#rpg-encounter-custom-submit');
|
||||||
const customSubmit = this.modal.querySelector('#rpg-encounter-custom-submit');
|
if (submitBtn && !submitBtn.disabled && !this.isProcessing) {
|
||||||
|
const input = this.modal.querySelector('#rpg-encounter-custom-input');
|
||||||
const submitCustomAction = async () => {
|
if (input) {
|
||||||
const action = customInput.value.trim();
|
const action = input.value.trim();
|
||||||
if (!action) return;
|
if (action) {
|
||||||
|
await this.processCombatAction(action);
|
||||||
await this.processCombatAction(action);
|
input.value = '';
|
||||||
customInput.value = '';
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (customSubmit) {
|
this._keypressHandler = async (e) => {
|
||||||
customSubmit.addEventListener('click', submitCustomAction);
|
const input = e.target.closest('#rpg-encounter-custom-input');
|
||||||
}
|
if (input && e.key === 'Enter' && !this.isProcessing) {
|
||||||
|
const action = input.value.trim();
|
||||||
if (customInput) {
|
if (action) {
|
||||||
customInput.addEventListener('keypress', (e) => {
|
await this.processCombatAction(action);
|
||||||
if (e.key === 'Enter') {
|
input.value = '';
|
||||||
submitCustomAction();
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// Attach to the modal itself (which never gets replaced)
|
||||||
|
this.modal.addEventListener('click', this._actionHandler);
|
||||||
|
this.modal.addEventListener('keypress', this._keypressHandler);
|
||||||
|
|
||||||
|
this._listenersAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -792,7 +809,7 @@ export class EncounterModal {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add action to log
|
// Add action to log
|
||||||
this.addToLog(`You: ${action}`, 'player-action');
|
this.addToLog(`${i18n.getTranslation('encounter.ui.youPrefix') || 'You: '}${action}`, 'player-action');
|
||||||
|
|
||||||
// Build and send combat action prompt
|
// Build and send combat action prompt
|
||||||
const actionPrompt = await buildCombatActionPrompt(action, currentEncounter.combatStats);
|
const actionPrompt = await buildCombatActionPrompt(action, currentEncounter.combatStats);
|
||||||
@@ -800,13 +817,13 @@ export class EncounterModal {
|
|||||||
// Store request for potential regeneration
|
// Store request for potential regeneration
|
||||||
this.lastRequest = { type: 'action', action, prompt: actionPrompt };
|
this.lastRequest = { type: 'action', action, prompt: actionPrompt };
|
||||||
|
|
||||||
const response = await generateRaw({
|
const response = await safeGenerateRaw({
|
||||||
prompt: actionPrompt,
|
prompt: actionPrompt,
|
||||||
quietToLoud: false
|
quietToLoud: false
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
this.showErrorWithRegenerate('No response received from AI. The model may be unavailable.');
|
this.showErrorWithRegenerate(i18n.getTranslation('encounter.ui.error.noResponse') || 'No response received from AI. The model may be unavailable.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -814,13 +831,14 @@ export class EncounterModal {
|
|||||||
const result = parseEncounterJSON(response);
|
const result = parseEncounterJSON(response);
|
||||||
|
|
||||||
if (!result || !result.combatStats) {
|
if (!result || !result.combatStats) {
|
||||||
this.showErrorWithRegenerate('Invalid JSON format detected. The AI returned malformed data. Ensure the Max Response Length is set to at least 2048 tokens, otherwise the model might run out of tokens and produce unfinished structures.');
|
this.showErrorWithRegenerate(i18n.getTranslation('encounter.ui.error.invalidJsonFormat') || 'Invalid JSON format detected. The AI returned malformed data. Ensure the Max Response Length is set to at least 2048 tokens, otherwise the model might run out of tokens and produce unfinished structures.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update encounter state
|
// Update encounter state
|
||||||
updateCurrentEncounter({
|
updateCurrentEncounter({
|
||||||
combatStats: result.combatStats
|
combatStats: result.combatStats,
|
||||||
|
playerActions: result.playerActions
|
||||||
});
|
});
|
||||||
|
|
||||||
// Collect log entries in order: enemy actions, party actions, then narration
|
// Collect log entries in order: enemy actions, party actions, then narration
|
||||||
@@ -881,7 +899,7 @@ export class EncounterModal {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[RPG Companion] Error processing combat action:', error);
|
console.error('[RPG Companion] Error processing combat action:', error);
|
||||||
this.showErrorWithRegenerate(`Error processing action: ${error.message}`);
|
this.showErrorWithRegenerate(`${i18n.getTranslation('encounter.ui.error.errorProcessingAction') || 'Error processing action:'} ${error.message}`);
|
||||||
|
|
||||||
// Re-enable buttons
|
// Re-enable buttons
|
||||||
this.modal.querySelectorAll('.rpg-encounter-action-btn, #rpg-encounter-custom-submit').forEach(btn => {
|
this.modal.querySelectorAll('.rpg-encounter-action-btn, #rpg-encounter-custom-submit').forEach(btn => {
|
||||||
@@ -935,16 +953,75 @@ export class EncounterModal {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-render controls if player died
|
// Re-render controls if player died OR if player's actions changed
|
||||||
const player = combatStats.party.find(m => m.isPlayer);
|
const player = combatStats.party.find(m => m.isPlayer);
|
||||||
|
const controlsContainer = this.modal.querySelector('.rpg-encounter-controls');
|
||||||
|
|
||||||
if (player && player.hp <= 0) {
|
if (player && player.hp <= 0) {
|
||||||
const controlsContainer = this.modal.querySelector('.rpg-encounter-controls');
|
|
||||||
if (controlsContainer) {
|
if (controlsContainer) {
|
||||||
controlsContainer.innerHTML = '<p class="rpg-encounter-defeated">You have been defeated...</p>';
|
controlsContainer.innerHTML = '<p class="rpg-encounter-defeated">' + (i18n.getTranslation('encounter.ui.youHaveBeenDefeated') || 'You have been defeated...') + '</p>';
|
||||||
|
}
|
||||||
|
} else if (currentEncounter.playerActions && controlsContainer) {
|
||||||
|
// Check if actions have changed by comparing with previous state
|
||||||
|
const actionsChanged = this.haveActionsChanged(currentEncounter.playerActions);
|
||||||
|
|
||||||
|
if (actionsChanged) {
|
||||||
|
// Store the new actions for next comparison
|
||||||
|
this._previousPlayerActions = {
|
||||||
|
attacks: currentEncounter.playerActions.attacks ? JSON.parse(JSON.stringify(currentEncounter.playerActions.attacks)) : [],
|
||||||
|
items: currentEncounter.playerActions.items ? [...currentEncounter.playerActions.items] : []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-render the entire controls section with new actions
|
||||||
|
const newControlsHTML = this.renderPlayerControls(combatStats.party, currentEncounter.playerActions);
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = newControlsHTML;
|
||||||
|
const newControls = tempDiv.firstElementChild;
|
||||||
|
|
||||||
|
if (newControls) {
|
||||||
|
controlsContainer.replaceWith(newControls);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if player's available actions have changed
|
||||||
|
* @param {Object} playerActions - Current player actions data with attacks and items
|
||||||
|
* @returns {boolean} True if actions changed
|
||||||
|
*/
|
||||||
|
haveActionsChanged(playerActions) {
|
||||||
|
if (!this._previousPlayerActions) {
|
||||||
|
// First time - store initial actions
|
||||||
|
this._previousPlayerActions = {
|
||||||
|
attacks: playerActions.attacks ? JSON.parse(JSON.stringify(playerActions.attacks)) : [],
|
||||||
|
items: playerActions.items ? [...playerActions.items] : []
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentAttacks = playerActions.attacks || [];
|
||||||
|
const currentItems = playerActions.items || [];
|
||||||
|
const prevAttacks = this._previousPlayerActions.attacks || [];
|
||||||
|
const prevItems = this._previousPlayerActions.items || [];
|
||||||
|
|
||||||
|
// Check if attacks changed
|
||||||
|
if (currentAttacks.length !== prevAttacks.length) return true;
|
||||||
|
for (let i = 0; i < currentAttacks.length; i++) {
|
||||||
|
const curr = typeof currentAttacks[i] === 'string' ? currentAttacks[i] : currentAttacks[i].name;
|
||||||
|
const prev = typeof prevAttacks[i] === 'string' ? prevAttacks[i] : prevAttacks[i].name;
|
||||||
|
if (curr !== prev) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if items changed
|
||||||
|
if (currentItems.length !== prevItems.length) return true;
|
||||||
|
for (let i = 0; i < currentItems.length; i++) {
|
||||||
|
if (currentItems[i] !== prevItems[i]) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds multiple log entries sequentially with animation
|
* Adds multiple log entries sequentially with animation
|
||||||
* @param {Array} entries - Array of {message, type} objects
|
* @param {Array} entries - Array of {message, type} objects
|
||||||
@@ -1002,7 +1079,7 @@ export class EncounterModal {
|
|||||||
// Generate summary
|
// Generate summary
|
||||||
const summaryPrompt = await buildCombatSummaryPrompt(currentEncounter.encounterLog, result);
|
const summaryPrompt = await buildCombatSummaryPrompt(currentEncounter.encounterLog, result);
|
||||||
|
|
||||||
const summaryResponse = await generateRaw({
|
const summaryResponse = await safeGenerateRaw({
|
||||||
prompt: summaryPrompt,
|
prompt: summaryPrompt,
|
||||||
quietToLoud: false
|
quietToLoud: false
|
||||||
});
|
});
|
||||||
@@ -1128,17 +1205,25 @@ export class EncounterModal {
|
|||||||
interrupted: '#888'
|
interrupted: '#888'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resultTexts = {
|
||||||
|
victory: i18n.getTranslation('encounter.ui.result.victory') || 'Victory',
|
||||||
|
defeat: i18n.getTranslation('encounter.ui.result.defeat') || 'Defeat',
|
||||||
|
fled: i18n.getTranslation('encounter.ui.result.fled') || 'Fled',
|
||||||
|
interrupted: i18n.getTranslation('encounter.ui.result.interrupted') || 'Interrupted'
|
||||||
|
};
|
||||||
|
|
||||||
const icon = resultIcons[result] || 'fa-flag-checkered';
|
const icon = resultIcons[result] || 'fa-flag-checkered';
|
||||||
const color = resultColors[result] || '#888';
|
const color = resultColors[result] || '#888';
|
||||||
|
const text = resultTexts[result] || result;
|
||||||
|
|
||||||
mainContent.innerHTML = `
|
mainContent.innerHTML = `
|
||||||
<div class="rpg-encounter-over" style="text-align: center; padding: 40px 20px;">
|
<div class="rpg-encounter-over" style="text-align: center; padding: 40px 20px;">
|
||||||
<i class="fa-solid ${icon}" style="font-size: 72px; color: ${color}; margin-bottom: 24px;"></i>
|
<i class="fa-solid ${icon}" style="font-size: 72px; color: ${color}; margin-bottom: 24px;"></i>
|
||||||
<h2 style="font-size: 32px; margin-bottom: 16px; text-transform: uppercase;">${result}</h2>
|
<h2 style="font-size: 32px; margin-bottom: 16px; text-transform: uppercase;">${text}</h2>
|
||||||
<p style="font-size: 18px; margin-bottom: 32px; opacity: 0.8;">Generating combat summary...</p>
|
<p style="font-size: 18px; margin-bottom: 32px; opacity: 0.8;">${i18n.getTranslation('encounter.ui.generatingCombatSummary') || 'Generating combat summary...'}</p>
|
||||||
<div class="rpg-encounter-loading" style="display: flex; justify-content: center; align-items: center; gap: 12px;">
|
<div class="rpg-encounter-loading" style="display: flex; justify-content: center; align-items: center; gap: 12px;">
|
||||||
<i class="fa-solid fa-spinner fa-spin" style="font-size: 24px;"></i>
|
<i class="fa-solid fa-spinner fa-spin" style="font-size: 24px;"></i>
|
||||||
<span>Please wait...</span>
|
<span>${i18n.getTranslation('encounter.ui.pleaseWait') || 'Please wait...'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -1157,12 +1242,13 @@ export class EncounterModal {
|
|||||||
if (!overScreen) return;
|
if (!overScreen) return;
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
overScreen.querySelector('p').textContent = speakerName
|
const message = speakerName
|
||||||
? `Combat summary has been added to the chat by ${speakerName}.`
|
? (i18n.getTranslation('encounter.ui.combatSummaryAddedBy') || 'Combat summary has been added to the chat by {speakerName}.').replace('{speakerName}', speakerName)
|
||||||
: 'Combat summary has been added to the chat.';
|
: (i18n.getTranslation('encounter.ui.combatSummaryAdded') || 'Combat summary has been added to the chat.');
|
||||||
|
overScreen.querySelector('p').textContent = message;
|
||||||
overScreen.querySelector('.rpg-encounter-loading').innerHTML = `
|
overScreen.querySelector('.rpg-encounter-loading').innerHTML = `
|
||||||
<button id="rpg-encounter-close-final" class="rpg-encounter-submit-btn" style="font-size: 18px; padding: 12px 24px;">
|
<button id="rpg-encounter-close-final" class="rpg-encounter-submit-btn" style="font-size: 18px; padding: 12px 24px;">
|
||||||
<i class="fa-solid fa-check"></i> Close Combat Window
|
<i class="fa-solid fa-check"></i> ${i18n.getTranslation('encounter.ui.closeCombatWindow') || 'Close Combat Window'}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -1174,11 +1260,11 @@ export class EncounterModal {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
overScreen.querySelector('p').textContent = 'Error generating combat summary.';
|
overScreen.querySelector('p').textContent = i18n.getTranslation('encounter.ui.errorGeneratingCombatSummary') || 'Error generating combat summary.';
|
||||||
overScreen.querySelector('.rpg-encounter-loading').innerHTML = `
|
overScreen.querySelector('.rpg-encounter-loading').innerHTML = `
|
||||||
<p style="color: #e94560;">Failed to create summary. You can close this window.</p>
|
<p style="color: #e94560;">${i18n.getTranslation('encounter.ui.failedToCreateSummary') || 'Failed to create summary. You can close this window.'}</p>
|
||||||
<button id="rpg-encounter-close-final" class="rpg-encounter-submit-btn" style="font-size: 18px; padding: 12px 24px; margin-top: 16px;">
|
<button id="rpg-encounter-close-final" class="rpg-encounter-submit-btn" style="font-size: 18px; padding: 12px 24px; margin-top: 16px;">
|
||||||
<i class="fa-solid fa-times"></i> Close Combat Window
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('encounter.ui.closeCombatWindow') || 'Close Combat Window'}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -1244,14 +1330,14 @@ export class EncounterModal {
|
|||||||
loadingContent.innerHTML = `
|
loadingContent.innerHTML = `
|
||||||
<div class="rpg-encounter-error-box">
|
<div class="rpg-encounter-error-box">
|
||||||
<i class="fa-solid fa-exclamation-triangle" style="color: #e94560; font-size: 48px; margin-bottom: 1em;"></i>
|
<i class="fa-solid fa-exclamation-triangle" style="color: #e94560; font-size: 48px; margin-bottom: 1em;"></i>
|
||||||
<p style="color: #e94560; font-weight: bold; font-size: 1.2em; margin: 0 0 0.5em 0;">Wrong Format Detected</p>
|
<p style="color: #e94560; font-weight: bold; font-size: 1.2em; margin: 0 0 0.5em 0;">${i18n.getTranslation('encounter.ui.wrongFormatDetected') || 'Wrong Format Detected'}</p>
|
||||||
<p style="color: var(--rpg-text, #ccc); margin: 0 0 1.5em 0; max-width: 500px;">${message}</p>
|
<p style="color: var(--rpg-text, #ccc); margin: 0 0 1.5em 0; max-width: 500px;">${message}</p>
|
||||||
<div style="display: flex; gap: 1em;">
|
<div style="display: flex; gap: 1em;">
|
||||||
<button id="rpg-error-regenerate" class="rpg-btn rpg-btn-primary">
|
<button id="rpg-error-regenerate" class="rpg-btn rpg-btn-primary">
|
||||||
<i class="fa-solid fa-rotate-right"></i> Regenerate
|
<i class="fa-solid fa-rotate-right"></i> ${i18n.getTranslation('encounter.ui.regenerate') || 'Regenerate'}
|
||||||
</button>
|
</button>
|
||||||
<button id="rpg-error-close" class="rpg-btn rpg-btn-secondary">
|
<button id="rpg-error-close" class="rpg-btn rpg-btn-secondary">
|
||||||
<i class="fa-solid fa-times"></i> Close
|
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.close') || 'Close'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { setupMobileTabs, removeMobileTabs } from './mobile.js';
|
import { setupMobileTabs, removeMobileTabs } from './mobile.js';
|
||||||
import { setupDesktopTabs, removeDesktopTabs } from './desktop.js';
|
import { setupDesktopTabs, removeDesktopTabs, updateStripWidgets } from './desktop.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles the visibility of plot buttons based on settings.
|
* Toggles the visibility of plot buttons based on settings.
|
||||||
@@ -144,7 +144,7 @@ export function updateCollapseToggleIcon() {
|
|||||||
*/
|
*/
|
||||||
export function setupCollapseToggle() {
|
export function setupCollapseToggle() {
|
||||||
const $collapseToggle = $('#rpg-collapse-toggle');
|
const $collapseToggle = $('#rpg-collapse-toggle');
|
||||||
$collapseToggle.attr('title', i18n.getTranslation('template.mainPanel.collapseExpand'));
|
$collapseToggle.attr('title', i18n.getTranslation('template.mainPanel.collapseExpand') || 'Collapse/Expand panel');
|
||||||
const $panel = $('#rpg-companion-panel');
|
const $panel = $('#rpg-companion-panel');
|
||||||
const $icon = $collapseToggle.find('i');
|
const $icon = $collapseToggle.find('i');
|
||||||
|
|
||||||
@@ -243,6 +243,9 @@ export function setupCollapseToggle() {
|
|||||||
} else if ($panel.hasClass('rpg-position-left')) {
|
} else if ($panel.hasClass('rpg-position-left')) {
|
||||||
$icon.removeClass('fa-chevron-left').addClass('fa-chevron-right');
|
$icon.removeClass('fa-chevron-left').addClass('fa-chevron-right');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update strip widgets when collapsing (they show in collapsed state)
|
||||||
|
updateStripWidgets();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -314,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();
|
||||||
@@ -325,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 {
|
||||||
@@ -334,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 {
|
||||||
@@ -343,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) {
|
||||||
@@ -431,6 +448,7 @@ export function updateGenerationModeUI() {
|
|||||||
if (extensionSettings.generationMode === 'together') {
|
if (extensionSettings.generationMode === 'together') {
|
||||||
// In "together" mode, manual update button is hidden
|
// In "together" mode, manual update button is hidden
|
||||||
$('#rpg-manual-update').hide();
|
$('#rpg-manual-update').hide();
|
||||||
|
$('#rpg-strip-refresh').hide();
|
||||||
$('#rpg-external-api-settings').slideUp(200);
|
$('#rpg-external-api-settings').slideUp(200);
|
||||||
$('#rpg-separate-mode-settings').slideUp(200);
|
$('#rpg-separate-mode-settings').slideUp(200);
|
||||||
// Hide auto-update toggle (not applicable in together mode)
|
// Hide auto-update toggle (not applicable in together mode)
|
||||||
@@ -438,6 +456,7 @@ export function updateGenerationModeUI() {
|
|||||||
} else if (extensionSettings.generationMode === 'separate') {
|
} else if (extensionSettings.generationMode === 'separate') {
|
||||||
// In "separate" mode, manual update button is visible
|
// In "separate" mode, manual update button is visible
|
||||||
$('#rpg-manual-update').show();
|
$('#rpg-manual-update').show();
|
||||||
|
$('#rpg-strip-refresh').show();
|
||||||
$('#rpg-external-api-settings').slideUp(200);
|
$('#rpg-external-api-settings').slideUp(200);
|
||||||
$('#rpg-separate-mode-settings').slideDown(200);
|
$('#rpg-separate-mode-settings').slideDown(200);
|
||||||
// Show auto-update toggle
|
// Show auto-update toggle
|
||||||
@@ -445,6 +464,7 @@ export function updateGenerationModeUI() {
|
|||||||
} else if (extensionSettings.generationMode === 'external') {
|
} else if (extensionSettings.generationMode === 'external') {
|
||||||
// In "external" mode, manual update button is visible AND both settings are shown
|
// In "external" mode, manual update button is visible AND both settings are shown
|
||||||
$('#rpg-manual-update').show();
|
$('#rpg-manual-update').show();
|
||||||
|
$('#rpg-strip-refresh').show();
|
||||||
$('#rpg-external-api-settings').slideDown(200);
|
$('#rpg-external-api-settings').slideDown(200);
|
||||||
$('#rpg-separate-mode-settings').slideDown(200);
|
$('#rpg-separate-mode-settings').slideDown(200);
|
||||||
// Show auto-update toggle for external mode too
|
// Show auto-update toggle for external mode too
|
||||||
|
|||||||
+463
-13
@@ -3,11 +3,12 @@
|
|||||||
* Handles mobile-specific UI functionality: FAB dragging, tabs, keyboard handling
|
* Handles mobile-specific UI functionality: FAB dragging, tabs, keyboard handling
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { extensionSettings } from '../../core/state.js';
|
import { extensionSettings, committedTrackerData, lastGeneratedData } from '../../core/state.js';
|
||||||
import { saveSettings } from '../../core/persistence.js';
|
import { saveSettings } from '../../core/persistence.js';
|
||||||
import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layout.js';
|
import { closeMobilePanelWithAnimation, updateCollapseToggleIcon } from './layout.js';
|
||||||
import { setupDesktopTabs, removeDesktopTabs } from './desktop.js';
|
import { setupDesktopTabs, removeDesktopTabs } from './desktop.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
import { hexToRgba } from './theme.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the text labels of the mobile navigation tabs based on the current language.
|
* Updates the text labels of the mobile navigation tabs based on the current language.
|
||||||
@@ -31,16 +32,35 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (translationKey) {
|
if (translationKey) {
|
||||||
const translation = i18n.getTranslation(translationKey);
|
let fallback = '';
|
||||||
if (translation) {
|
switch (tabName) {
|
||||||
$tab.find('span').text(translation);
|
case 'stats':
|
||||||
|
fallback = 'Status';
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
fallback = 'Info';
|
||||||
|
break;
|
||||||
|
case 'inventory':
|
||||||
|
fallback = 'Inventory';
|
||||||
|
break;
|
||||||
|
case 'equipment':
|
||||||
|
fallback = 'Equipment';
|
||||||
|
break;
|
||||||
|
case 'quests':
|
||||||
|
fallback = 'Quests';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
const translation = i18n.getTranslation(translationKey) || fallback;
|
||||||
|
$tab.find('span').text(translation);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -106,6 +126,14 @@ export function setupMobileToggle() {
|
|||||||
right: 'auto',
|
right: 'auto',
|
||||||
bottom: 'auto'
|
bottom: 'auto'
|
||||||
});
|
});
|
||||||
|
// Also update widget container position during drag
|
||||||
|
const $container = $('#rpg-fab-widget-container');
|
||||||
|
if ($container.length > 0) {
|
||||||
|
$container.css({
|
||||||
|
top: pendingY + 'px',
|
||||||
|
left: pendingX + 'px'
|
||||||
|
});
|
||||||
|
}
|
||||||
pendingX = null;
|
pendingX = null;
|
||||||
pendingY = null;
|
pendingY = null;
|
||||||
}
|
}
|
||||||
@@ -253,7 +281,10 @@ export function setupMobileToggle() {
|
|||||||
// console.log('[RPG Mobile] Saved new FAB position (mouse):', newPosition);
|
// console.log('[RPG Mobile] Saved new FAB position (mouse):', newPosition);
|
||||||
|
|
||||||
// Constrain to viewport bounds (now that position is saved)
|
// Constrain to viewport bounds (now that position is saved)
|
||||||
setTimeout(() => constrainFabToViewport(), 10);
|
setTimeout(() => {
|
||||||
|
constrainFabToViewport();
|
||||||
|
updateFabWidgetPosition(); // Update widget container position
|
||||||
|
}, 10);
|
||||||
|
|
||||||
// Re-enable transitions with smooth animation
|
// Re-enable transitions with smooth animation
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -294,7 +325,10 @@ export function setupMobileToggle() {
|
|||||||
// console.log('[RPG Mobile] Saved new FAB position:', newPosition);
|
// console.log('[RPG Mobile] Saved new FAB position:', newPosition);
|
||||||
|
|
||||||
// Constrain to viewport bounds (now that position is saved)
|
// Constrain to viewport bounds (now that position is saved)
|
||||||
setTimeout(() => constrainFabToViewport(), 10);
|
setTimeout(() => {
|
||||||
|
constrainFabToViewport();
|
||||||
|
updateFabWidgetPosition(); // Update widget container position
|
||||||
|
}, 10);
|
||||||
|
|
||||||
// Re-enable transitions with smooth animation
|
// Re-enable transitions with smooth animation
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -578,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,23 +625,28 @@ 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)
|
||||||
if (hasStats) {
|
if (hasStats) {
|
||||||
tabs.push('<button class="rpg-mobile-tab active" data-tab="stats"><i class="fa-solid fa-chart-bar"></i><span>' + i18n.getTranslation('global.status') + '</span></button>');
|
tabs.push('<button class="rpg-mobile-tab active" data-tab="stats"><i class="fa-solid fa-chart-bar"></i><span>' + (i18n.getTranslation('global.status') || 'Status') + '</span></button>');
|
||||||
}
|
}
|
||||||
// Tab 2: Info (Info Box + Character Thoughts)
|
// Tab 2: Info (Info Box + Character Thoughts)
|
||||||
if (hasInfo) {
|
if (hasInfo) {
|
||||||
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="info"><i class="fa-solid fa-book"></i><span>' + i18n.getTranslation('global.info') + '</span></button>');
|
tabs.push('<button class="rpg-mobile-tab ' + (tabs.length === 0 ? 'active' : '') + '" data-tab="info"><i class="fa-solid fa-book"></i><span>' + (i18n.getTranslation('global.info') || 'Info') + '</span></button>');
|
||||||
}
|
}
|
||||||
// Tab 3: Inventory
|
// Tab 3: Inventory
|
||||||
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') + '</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') + '</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>');
|
||||||
}
|
}
|
||||||
|
|
||||||
const $tabNav = $('<div class="rpg-mobile-tabs">' + tabs.join('') + '</div>');
|
const $tabNav = $('<div class="rpg-mobile-tabs">' + tabs.join('') + '</div>');
|
||||||
@@ -616,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)
|
||||||
@@ -649,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());
|
||||||
@@ -667,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
|
||||||
@@ -695,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
|
||||||
@@ -704,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);
|
||||||
@@ -732,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();
|
||||||
}
|
}
|
||||||
@@ -779,12 +834,17 @@ export function setupMobileKeyboardHandling() {
|
|||||||
/**
|
/**
|
||||||
* Handles focus on contenteditable fields to ensure they're visible when keyboard appears.
|
* Handles focus on contenteditable fields to ensure they're visible when keyboard appears.
|
||||||
* Uses smooth scrolling to bring focused field into view with proper padding.
|
* Uses smooth scrolling to bring focused field into view with proper padding.
|
||||||
|
* Only applies on mobile viewports where virtual keyboard can obscure content.
|
||||||
*/
|
*/
|
||||||
export function setupContentEditableScrolling() {
|
export function setupContentEditableScrolling() {
|
||||||
const $panel = $('#rpg-companion-panel');
|
const $panel = $('#rpg-companion-panel');
|
||||||
|
|
||||||
// Use event delegation for all contenteditable fields
|
// Use event delegation for all contenteditable fields
|
||||||
$panel.on('focusin', '[contenteditable="true"]', function(e) {
|
$panel.on('focusin', '[contenteditable="true"]', function(e) {
|
||||||
|
// Only apply scrolling behavior on mobile (where virtual keyboard appears)
|
||||||
|
const isMobile = window.innerWidth <= 1000;
|
||||||
|
if (!isMobile) return;
|
||||||
|
|
||||||
const $field = $(this);
|
const $field = $(this);
|
||||||
|
|
||||||
// Small delay to let keyboard animate in
|
// Small delay to let keyboard animate in
|
||||||
@@ -1230,3 +1290,393 @@ export function setupDebugButtonDrag() {
|
|||||||
isDragging = false;
|
isDragging = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// FAB WIDGETS - Info display around FAB button
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the FAB widgets display based on current tracker data and settings.
|
||||||
|
* Widgets are positioned in 8 positions around the FAB (N, NE, E, SE, S, SW, W, NW).
|
||||||
|
*/
|
||||||
|
export function updateFabWidgets() {
|
||||||
|
const $fab = $('#rpg-mobile-toggle');
|
||||||
|
if ($fab.length === 0) return;
|
||||||
|
|
||||||
|
// Remove existing widget container and clean up event listeners
|
||||||
|
$('#rpg-fab-widget-container').remove();
|
||||||
|
$(document).off('click.fabWidgets touchstart.fabWidgets');
|
||||||
|
|
||||||
|
// Check if widgets are enabled
|
||||||
|
const widgetSettings = extensionSettings.mobileFabWidgets;
|
||||||
|
if (!widgetSettings || !widgetSettings.enabled) return;
|
||||||
|
|
||||||
|
// Don't show widgets on desktop or when panel is open
|
||||||
|
if (window.innerWidth > 1000) return;
|
||||||
|
|
||||||
|
// Get tracker data - prefer lastGeneratedData (most recent) over committedTrackerData
|
||||||
|
const infoBox = lastGeneratedData?.infoBox || committedTrackerData?.infoBox;
|
||||||
|
const userStats = lastGeneratedData?.userStats || committedTrackerData?.userStats;
|
||||||
|
|
||||||
|
// Parse infoBox if it's a string
|
||||||
|
let infoData = null;
|
||||||
|
if (infoBox) {
|
||||||
|
try {
|
||||||
|
infoData = typeof infoBox === 'string' ? JSON.parse(infoBox) : infoBox;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG FAB Widgets] Failed to parse infoBox:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse userStats if it's a string
|
||||||
|
let statsData = null;
|
||||||
|
if (userStats) {
|
||||||
|
try {
|
||||||
|
statsData = typeof userStats === 'string' ? JSON.parse(userStats) : userStats;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG FAB Widgets] Failed to parse userStats:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create widget container positioned at FAB location
|
||||||
|
const fabOffset = $fab.offset();
|
||||||
|
const fabWidth = $fab.outerWidth();
|
||||||
|
const fabHeight = $fab.outerHeight();
|
||||||
|
|
||||||
|
const $container = $('<div id="rpg-fab-widget-container" class="rpg-fab-widget-container"></div>');
|
||||||
|
$container.css({
|
||||||
|
top: fabOffset.top + 'px',
|
||||||
|
left: fabOffset.left + 'px',
|
||||||
|
width: fabWidth + 'px',
|
||||||
|
height: fabHeight + 'px'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build widgets based on settings - auto-assign positions sequentially
|
||||||
|
const widgets = [];
|
||||||
|
|
||||||
|
// Collect enabled widgets in display priority order
|
||||||
|
// Large widgets (Stats, Attributes) go to West/Northwest
|
||||||
|
// Small widgets spread around other positions
|
||||||
|
|
||||||
|
// Weather Icon (small)
|
||||||
|
if (widgetSettings.weatherIcon?.enabled && infoData?.weather?.emoji) {
|
||||||
|
widgets.push({
|
||||||
|
type: 'small',
|
||||||
|
html: `<div class="rpg-fab-widget rpg-fab-widget-weather-icon" title="Weather">${infoData.weather.emoji}</div>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather Description (small)
|
||||||
|
if (widgetSettings.weatherDesc?.enabled && infoData?.weather?.forecast) {
|
||||||
|
const desc = infoData.weather.forecast.length > 15 ? infoData.weather.forecast.substring(0, 13) + '…' : infoData.weather.forecast;
|
||||||
|
widgets.push({
|
||||||
|
type: 'small',
|
||||||
|
html: `<div class="rpg-fab-widget rpg-fab-widget-weather-desc" title="${infoData.weather.forecast}">${desc}</div>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to create expandable text widget HTML
|
||||||
|
const createExpandableText = (fullText, maxLen, emoji) => {
|
||||||
|
if (fullText.length <= maxLen) {
|
||||||
|
return `${emoji} ${fullText}`;
|
||||||
|
}
|
||||||
|
const truncated = fullText.substring(0, maxLen - 2) + '…';
|
||||||
|
return `${emoji} <span class="rpg-truncated">${truncated}</span><span class="rpg-full-text">${fullText}</span>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if text needs truncation for data attribute
|
||||||
|
const needsExpand = (text, maxLen) => text.length > maxLen;
|
||||||
|
|
||||||
|
// Helper to parse time string and calculate clock hand angles
|
||||||
|
const parseTimeForClock = (timeStr) => {
|
||||||
|
const timeMatch = timeStr.match(/(\d+):(\d+)/);
|
||||||
|
if (timeMatch) {
|
||||||
|
const hours = parseInt(timeMatch[1]);
|
||||||
|
const minutes = parseInt(timeMatch[2]);
|
||||||
|
const hourAngle = (hours % 12) * 30 + minutes * 0.5; // 30° per hour + 0.5° per minute
|
||||||
|
const minuteAngle = minutes * 6; // 6° per minute
|
||||||
|
return { hourAngle, minuteAngle };
|
||||||
|
}
|
||||||
|
return { hourAngle: 0, minuteAngle: 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clock/Time (bottom position with animated clock face)
|
||||||
|
if (widgetSettings.clock?.enabled && infoData?.time) {
|
||||||
|
const timeStr = infoData.time.end || infoData.time.value || infoData.time.start || '';
|
||||||
|
if (timeStr) {
|
||||||
|
const { hourAngle, minuteAngle } = parseTimeForClock(timeStr);
|
||||||
|
widgets.push({
|
||||||
|
type: 'bottom', // Special type for bottom position
|
||||||
|
html: `<div class="rpg-fab-widget rpg-fab-widget-clock" title="${timeStr}">
|
||||||
|
<div class="rpg-fab-clock-face">
|
||||||
|
<div class="rpg-fab-clock-hour" style="transform: rotate(${hourAngle}deg)"></div>
|
||||||
|
<div class="rpg-fab-clock-minute" style="transform: rotate(${minuteAngle}deg)"></div>
|
||||||
|
<div class="rpg-fab-clock-center"></div>
|
||||||
|
</div>
|
||||||
|
<span class="rpg-fab-clock-time">${timeStr}</span>
|
||||||
|
</div>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date (small)
|
||||||
|
if (widgetSettings.date?.enabled && infoData?.date?.value) {
|
||||||
|
const dateVal = infoData.date.value;
|
||||||
|
const expandAttr = needsExpand(dateVal, 12) ? ' data-full-text="true"' : '';
|
||||||
|
widgets.push({
|
||||||
|
type: 'small',
|
||||||
|
html: `<div class="rpg-fab-widget rpg-fab-widget-date"${expandAttr} title="${dateVal}">${createExpandableText(dateVal, 12, '📅')}</div>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location (small)
|
||||||
|
if (widgetSettings.location?.enabled && infoData?.location?.value) {
|
||||||
|
const loc = infoData.location.value;
|
||||||
|
const expandAttr = needsExpand(loc, 14) ? ' data-full-text="true"' : '';
|
||||||
|
widgets.push({
|
||||||
|
type: 'small',
|
||||||
|
html: `<div class="rpg-fab-widget rpg-fab-widget-location"${expandAttr} title="${loc}">${createExpandableText(loc, 14, '📍')}</div>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats (large - goes to West) - respects trackerConfig.userStats.customStats
|
||||||
|
// Use extensionSettings.userStats as primary source (contains all stats), fallback to committedTrackerData
|
||||||
|
let allStats = [];
|
||||||
|
try {
|
||||||
|
const userStatsJson = extensionSettings.userStats;
|
||||||
|
const parsedUserStats = typeof userStatsJson === 'string' ? JSON.parse(userStatsJson) : userStatsJson;
|
||||||
|
if (parsedUserStats?.stats) {
|
||||||
|
allStats = parsedUserStats.stats;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RPG FAB Widgets] Failed to parse extensionSettings.userStats:', e);
|
||||||
|
}
|
||||||
|
// Fallback to statsData if extensionSettings.userStats is empty
|
||||||
|
if (allStats.length === 0 && statsData?.stats) {
|
||||||
|
allStats = statsData.stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widgetSettings.stats?.enabled && allStats.length > 0) {
|
||||||
|
// Get enabled stats from trackerConfig - match by id (lowercase)
|
||||||
|
const configuredStats = extensionSettings.trackerConfig?.userStats?.customStats || [];
|
||||||
|
const enabledStatMap = new Map();
|
||||||
|
configuredStats.forEach(s => {
|
||||||
|
if (s.enabled !== false) {
|
||||||
|
enabledStatMap.set(s.id?.toLowerCase(), true);
|
||||||
|
enabledStatMap.set(s.name?.toLowerCase(), true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const statsHtml = allStats
|
||||||
|
.filter(s => {
|
||||||
|
// If no config, show all stats
|
||||||
|
if (configuredStats.length === 0) return true;
|
||||||
|
// Check if stat is enabled in trackerConfig (match by id or name, case-insensitive)
|
||||||
|
const statId = s.id?.toLowerCase();
|
||||||
|
const statName = s.name?.toLowerCase();
|
||||||
|
return enabledStatMap.has(statId) || enabledStatMap.has(statName);
|
||||||
|
})
|
||||||
|
.map(stat => {
|
||||||
|
const value = typeof stat.value === 'number' ? stat.value : parseInt(stat.value) || 0;
|
||||||
|
const color = getStatColor(value);
|
||||||
|
const abbr = stat.name.substring(0, 3).toUpperCase();
|
||||||
|
return `<span class="rpg-fab-widget-stat-item" title="${stat.name}: ${value}" style="color: ${color};">${abbr}:${value}</span>`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
if (statsHtml) {
|
||||||
|
widgets.push({
|
||||||
|
type: 'large',
|
||||||
|
preferredPos: 6, // West
|
||||||
|
html: `<div class="rpg-fab-widget rpg-fab-widget-stats"><div class="rpg-fab-widget-stats-row">${statsHtml}</div></div>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPG Attributes (large - goes to Northwest) - respects trackerConfig.userStats.rpgAttributes
|
||||||
|
if (widgetSettings.attributes?.enabled) {
|
||||||
|
// Check if RPG attributes are enabled in trackerConfig
|
||||||
|
const showRPGAttributes = extensionSettings.trackerConfig?.userStats?.showRPGAttributes !== false;
|
||||||
|
|
||||||
|
if (showRPGAttributes && extensionSettings.classicStats) {
|
||||||
|
// Get enabled attributes from trackerConfig
|
||||||
|
const configuredAttrs = extensionSettings.trackerConfig?.userStats?.rpgAttributes || [];
|
||||||
|
const enabledAttrIds = configuredAttrs.filter(a => a.enabled !== false).map(a => a.id);
|
||||||
|
|
||||||
|
const attrs = extensionSettings.classicStats;
|
||||||
|
const attrItems = Object.entries(attrs)
|
||||||
|
.filter(([key]) => {
|
||||||
|
// Check if attribute is enabled in trackerConfig
|
||||||
|
if (enabledAttrIds.length > 0) {
|
||||||
|
return enabledAttrIds.includes(key.toLowerCase());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(([key, value]) => `<div class="rpg-fab-widget-attr-item"><span class="rpg-fab-widget-attr-name">${key.toUpperCase()}</span><span class="rpg-fab-widget-attr-value">${value}</span></div>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
if (attrItems) {
|
||||||
|
widgets.push({
|
||||||
|
type: 'large',
|
||||||
|
preferredPos: 7, // Northwest
|
||||||
|
html: `<div class="rpg-fab-widget rpg-fab-widget-attributes" title="Attributes"><div class="rpg-fab-widget-attr-grid">${attrItems}</div></div>`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-assign positions intelligently
|
||||||
|
// Large widgets get their preferred positions first (West=6, Northwest=7)
|
||||||
|
// Bottom widgets get position 4 (South)
|
||||||
|
// Small widgets fill remaining positions clockwise from North (0)
|
||||||
|
const usedPositions = new Set();
|
||||||
|
const positionedWidgets = [];
|
||||||
|
|
||||||
|
// Position order for small widgets: N(0), NE(1), E(2), SE(3), SW(5) - skip S(4) for bottom/clock
|
||||||
|
const smallPositionOrder = [0, 1, 2, 3, 5];
|
||||||
|
let smallPosIndex = 0;
|
||||||
|
|
||||||
|
// Check if only one large widget exists (for centering)
|
||||||
|
const largeWidgets = widgets.filter(w => w.type === 'large');
|
||||||
|
const singleLargeWidget = largeWidgets.length === 1;
|
||||||
|
|
||||||
|
// First: assign bottom widgets to position 4 (South)
|
||||||
|
widgets.filter(w => w.type === 'bottom').forEach(w => {
|
||||||
|
const pos = 4; // South position
|
||||||
|
usedPositions.add(pos);
|
||||||
|
const finalHtml = w.html.replace('class="rpg-fab-widget', `class="rpg-fab-widget rpg-fab-widget-pos-${pos}`);
|
||||||
|
positionedWidgets.push({ position: pos, html: finalHtml });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Second: assign large widgets to their preferred positions
|
||||||
|
largeWidgets.forEach(w => {
|
||||||
|
let pos = w.preferredPos;
|
||||||
|
// If preferred position is taken, find next available from large positions
|
||||||
|
if (usedPositions.has(pos)) {
|
||||||
|
pos = pos === 6 ? 7 : 6; // Try the other large position
|
||||||
|
}
|
||||||
|
usedPositions.add(pos);
|
||||||
|
// Add centered class if this is the only large widget
|
||||||
|
const centeredClass = singleLargeWidget ? ' rpg-fab-widget-centered' : '';
|
||||||
|
const finalHtml = w.html.replace('class="rpg-fab-widget', `class="rpg-fab-widget rpg-fab-widget-pos-${pos}${centeredClass}`);
|
||||||
|
positionedWidgets.push({ position: pos, html: finalHtml });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Third: assign small widgets to remaining positions
|
||||||
|
widgets.filter(w => w.type === 'small').forEach(w => {
|
||||||
|
// Find next available position from small position order
|
||||||
|
while (smallPosIndex < smallPositionOrder.length && usedPositions.has(smallPositionOrder[smallPosIndex])) {
|
||||||
|
smallPosIndex++;
|
||||||
|
}
|
||||||
|
const pos = smallPosIndex < smallPositionOrder.length ? smallPositionOrder[smallPosIndex] : (smallPosIndex % 8);
|
||||||
|
usedPositions.add(pos);
|
||||||
|
smallPosIndex++;
|
||||||
|
const finalHtml = w.html.replace('class="rpg-fab-widget', `class="rpg-fab-widget rpg-fab-widget-pos-${pos}`);
|
||||||
|
positionedWidgets.push({ position: pos, html: finalHtml });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add widgets to container
|
||||||
|
positionedWidgets.forEach(w => $container.append(w.html));
|
||||||
|
|
||||||
|
// Append container to body
|
||||||
|
if (positionedWidgets.length > 0) {
|
||||||
|
$('body').append($container);
|
||||||
|
|
||||||
|
// Add mobile tap handler for expandable widgets
|
||||||
|
$container.find('.rpg-fab-widget[data-full-text]').on('click touchstart', function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
const $this = $(this);
|
||||||
|
const wasExpanded = $this.hasClass('expanded');
|
||||||
|
|
||||||
|
// Collapse all other expanded widgets
|
||||||
|
$container.find('.rpg-fab-widget.expanded').removeClass('expanded');
|
||||||
|
|
||||||
|
// Toggle this one
|
||||||
|
if (!wasExpanded) {
|
||||||
|
$this.addClass('expanded');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collapse on tap outside
|
||||||
|
$(document).on('click.fabWidgets touchstart.fabWidgets', function(e) {
|
||||||
|
if (!$(e.target).closest('.rpg-fab-widget').length) {
|
||||||
|
$container.find('.rpg-fab-widget.expanded').removeClass('expanded');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a color for a stat value (0-100) using a gradient from low to high.
|
||||||
|
* @param {number} value - The stat value (0-100)
|
||||||
|
* @returns {string} CSS color value
|
||||||
|
*/
|
||||||
|
function getStatColor(value) {
|
||||||
|
const lowColor = extensionSettings.statBarColorLow || '#cc3333';
|
||||||
|
const lowOpacity = extensionSettings.statBarColorLowOpacity ?? 100;
|
||||||
|
const highColor = extensionSettings.statBarColorHigh || '#33cc66';
|
||||||
|
const highOpacity = extensionSettings.statBarColorHighOpacity ?? 100;
|
||||||
|
|
||||||
|
// Simple linear interpolation between low and high colors
|
||||||
|
const percent = Math.min(100, Math.max(0, value)) / 100;
|
||||||
|
|
||||||
|
// Parse colors
|
||||||
|
const lowRGB = hexToRgb(lowColor);
|
||||||
|
const highRGB = hexToRgb(highColor);
|
||||||
|
|
||||||
|
if (!lowRGB || !highRGB) return value > 50 ? hexToRgba(highColor, highOpacity) : hexToRgba(lowColor, lowOpacity);
|
||||||
|
|
||||||
|
const r = Math.round(lowRGB.r + (highRGB.r - lowRGB.r) * percent);
|
||||||
|
const g = Math.round(lowRGB.g + (highRGB.g - lowRGB.g) * percent);
|
||||||
|
const b = Math.round(lowRGB.b + (highRGB.b - lowRGB.b) * percent);
|
||||||
|
const a = (lowOpacity + (highOpacity - lowOpacity) * percent) / 100;
|
||||||
|
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a hex color to RGB object.
|
||||||
|
* @param {string} hex - Hex color string (e.g., "#cc3333")
|
||||||
|
* @returns {{r: number, g: number, b: number}|null}
|
||||||
|
*/
|
||||||
|
function hexToRgb(hex) {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
return result ? {
|
||||||
|
r: parseInt(result[1], 16),
|
||||||
|
g: parseInt(result[2], 16),
|
||||||
|
b: parseInt(result[3], 16)
|
||||||
|
} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the FAB widget container position to match FAB button position.
|
||||||
|
* Call this after FAB is dragged.
|
||||||
|
*/
|
||||||
|
export function updateFabWidgetPosition() {
|
||||||
|
const $fab = $('#rpg-mobile-toggle');
|
||||||
|
const $container = $('#rpg-fab-widget-container');
|
||||||
|
|
||||||
|
if ($fab.length === 0 || $container.length === 0) return;
|
||||||
|
|
||||||
|
const fabOffset = $fab.offset();
|
||||||
|
$container.css({
|
||||||
|
top: fabOffset.top + 'px',
|
||||||
|
left: fabOffset.left + 'px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the FAB loading state (spinning animation during API requests).
|
||||||
|
* @param {boolean} loading - Whether to show loading state
|
||||||
|
*/
|
||||||
|
export function setFabLoadingState(loading) {
|
||||||
|
const $fab = $('#rpg-mobile-toggle');
|
||||||
|
if ($fab.length === 0) return;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
$fab.addClass('rpg-fab-loading');
|
||||||
|
} else {
|
||||||
|
$fab.removeClass('rpg-fab-loading');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
$infoBoxContainer,
|
$infoBoxContainer,
|
||||||
$thoughtsContainer,
|
$thoughtsContainer,
|
||||||
$userStatsContainer,
|
$userStatsContainer,
|
||||||
|
clearThoughtBasedExpressionPortraits,
|
||||||
setPendingDiceRoll,
|
setPendingDiceRoll,
|
||||||
getPendingDiceRoll,
|
getPendingDiceRoll,
|
||||||
clearSessionAvatarPrompts
|
clearSessionAvatarPrompts
|
||||||
@@ -21,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,
|
||||||
@@ -323,7 +325,7 @@ export function setupDiceRoller() {
|
|||||||
e.stopPropagation(); // Prevent opening the dice popup
|
e.stopPropagation(); // Prevent opening the dice popup
|
||||||
clearDiceRollCore();
|
clearDiceRollCore();
|
||||||
});
|
});
|
||||||
$('#rpg-clear-dice').attr('title', i18n.getTranslation('template.mainPanel.clearLastRoll'));
|
$('#rpg-clear-dice').attr('title', i18n.getTranslation('template.mainPanel.clearLastRoll') || 'Clear last roll');
|
||||||
|
|
||||||
return diceModal;
|
return diceModal;
|
||||||
}
|
}
|
||||||
@@ -370,6 +372,7 @@ export function setupSettingsPopup() {
|
|||||||
|
|
||||||
// Clear session avatar prompts
|
// Clear session avatar prompts
|
||||||
clearSessionAvatarPrompts();
|
clearSessionAvatarPrompts();
|
||||||
|
clearThoughtBasedExpressionPortraits();
|
||||||
|
|
||||||
// Clear chat metadata immediately (don't wait for debounced save)
|
// Clear chat metadata immediately (don't wait for debounced save)
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
@@ -387,6 +390,14 @@ export function setupSettingsPopup() {
|
|||||||
delete message.extra.rpg_companion_swipes;
|
delete message.extra.rpg_companion_swipes;
|
||||||
// console.log('[RPG Companion] Cleared swipe data from message at index', i);
|
// console.log('[RPG Companion] Cleared swipe data from message at index', i);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(message.swipe_info)) {
|
||||||
|
for (const swipeInfo of message.swipe_info) {
|
||||||
|
if (swipeInfo?.extra?.rpg_companion_swipes) {
|
||||||
|
delete swipeInfo.extra.rpg_companion_swipes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,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');
|
||||||
@@ -602,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
|
||||||
@@ -653,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 });
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { extensionSettings } from '../../core/state.js';
|
import { extensionSettings } from '../../core/state.js';
|
||||||
import { saveSettings } from '../../core/persistence.js';
|
import { saveSettings } from '../../core/persistence.js';
|
||||||
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_SPOTIFY_PROMPT, DEFAULT_NARRATOR_PROMPT } from '../generation/promptBuilder.js';
|
import { DEFAULT_HTML_PROMPT, DEFAULT_DIALOGUE_COLORING_PROMPT, DEFAULT_DECEPTION_PROMPT, DEFAULT_OMNISCIENCE_FILTER_PROMPT, DEFAULT_CYOA_PROMPT, DEFAULT_SPOTIFY_PROMPT, DEFAULT_NARRATOR_PROMPT, DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT } from '../generation/promptBuilder.js';
|
||||||
|
|
||||||
let $editorModal = null;
|
let $editorModal = null;
|
||||||
let tempPrompts = null; // Temporary prompts for cancel functionality
|
let tempPrompts = null; // Temporary prompts for cancel functionality
|
||||||
@@ -13,8 +13,12 @@ let tempPrompts = null; // Temporary prompts for cancel functionality
|
|||||||
const DEFAULT_PROMPTS = {
|
const DEFAULT_PROMPTS = {
|
||||||
html: DEFAULT_HTML_PROMPT,
|
html: DEFAULT_HTML_PROMPT,
|
||||||
dialogueColoring: DEFAULT_DIALOGUE_COLORING_PROMPT,
|
dialogueColoring: DEFAULT_DIALOGUE_COLORING_PROMPT,
|
||||||
|
deception: DEFAULT_DECEPTION_PROMPT,
|
||||||
|
omniscience: DEFAULT_OMNISCIENCE_FILTER_PROMPT,
|
||||||
|
cyoa: DEFAULT_CYOA_PROMPT,
|
||||||
spotify: DEFAULT_SPOTIFY_PROMPT,
|
spotify: DEFAULT_SPOTIFY_PROMPT,
|
||||||
narrator: DEFAULT_NARRATOR_PROMPT,
|
narrator: DEFAULT_NARRATOR_PROMPT,
|
||||||
|
contextInstructions: DEFAULT_CONTEXT_INSTRUCTIONS_PROMPT,
|
||||||
plotRandom: 'Actually, the scene is getting stale. Introduce {{random::stakes::a plot twist::a new character::a cataclysm::a fourth-wall-breaking joke::a sudden atmospheric phenomenon::a plot hook::a running gag::an ecchi scenario::Death from Discworld::a new stake::a drama::a conflict::an angered entity::a god::a vision::a prophetic dream::Il Dottore from Genshin Impact::a new development::a civilian in need::an emotional bit::a threat::a villain::an important memory recollection::a marriage proposal::a date idea::an angry horde of villagers with pitchforks::a talking animal::an enemy::a cliffhanger::a short omniscient POV shift to a completely different character::a quest::an unexpected revelation::a scandal::an evil clone::death of an important character::harm to an important character::a romantic setup::a gossip::a messenger::a plot point from the past::a plot hole::a tragedy::a ghost::an otherworldly occurrence::a plot device::a curse::a magic device::a rival::an unexpected pregnancy::a brothel::a prostitute::a new location::a past lover::a completely random thing::a what-if scenario::a significant choice::war::love::a monster::lewd undertones::Professor Mari::a travelling troupe::a secret::a fortune-teller::something completely different::a killer::a murder mystery::a mystery::a skill check::a deus ex machina::three raccoons in a trench coat::a pet::a slave::an orphan::a psycho::tentacles::"there is only one bed" trope::accidental marriage::a fun twist::a boss battle::sexy corn::an eldritch horror::a character getting hungry, thirsty, or exhausted::horniness::a need for a bathroom break need::someone fainting::an assassination attempt::a meta narration of this all being an out of hand DND session::a dungeon::a friend in need::an old friend::a small time skip::a scene shift::Aurora Borealis, at this time of year, at this time of day, at this part of the country::a grand ball::a surprise party::zombies::foreshadowing::a Spanish Inquisition (nobody expects it)::a natural plot progression}} to make things more interesting! Be creative, but stay grounded in the setting.',
|
plotRandom: 'Actually, the scene is getting stale. Introduce {{random::stakes::a plot twist::a new character::a cataclysm::a fourth-wall-breaking joke::a sudden atmospheric phenomenon::a plot hook::a running gag::an ecchi scenario::Death from Discworld::a new stake::a drama::a conflict::an angered entity::a god::a vision::a prophetic dream::Il Dottore from Genshin Impact::a new development::a civilian in need::an emotional bit::a threat::a villain::an important memory recollection::a marriage proposal::a date idea::an angry horde of villagers with pitchforks::a talking animal::an enemy::a cliffhanger::a short omniscient POV shift to a completely different character::a quest::an unexpected revelation::a scandal::an evil clone::death of an important character::harm to an important character::a romantic setup::a gossip::a messenger::a plot point from the past::a plot hole::a tragedy::a ghost::an otherworldly occurrence::a plot device::a curse::a magic device::a rival::an unexpected pregnancy::a brothel::a prostitute::a new location::a past lover::a completely random thing::a what-if scenario::a significant choice::war::love::a monster::lewd undertones::Professor Mari::a travelling troupe::a secret::a fortune-teller::something completely different::a killer::a murder mystery::a mystery::a skill check::a deus ex machina::three raccoons in a trench coat::a pet::a slave::an orphan::a psycho::tentacles::"there is only one bed" trope::accidental marriage::a fun twist::a boss battle::sexy corn::an eldritch horror::a character getting hungry, thirsty, or exhausted::horniness::a need for a bathroom break need::someone fainting::an assassination attempt::a meta narration of this all being an out of hand DND session::a dungeon::a friend in need::an old friend::a small time skip::a scene shift::Aurora Borealis, at this time of year, at this time of day, at this part of the country::a grand ball::a surprise party::zombies::foreshadowing::a Spanish Inquisition (nobody expects it)::a natural plot progression}} to make things more interesting! Be creative, but stay grounded in the setting.',
|
||||||
plotNatural: 'Actually, the scene is getting stale. Progress it, to make things more interesting! Reintroduce an unresolved plot point from the past, or push the story further towards the current main goal. Be creative, but stay grounded in the setting.',
|
plotNatural: 'Actually, the scene is getting stale. Progress it, to make things more interesting! Reintroduce an unresolved plot point from the past, or push the story further towards the current main goal. Be creative, but stay grounded in the setting.',
|
||||||
avatar: `You are a visionary artist trapped in a cage of logic. Your mind is filled with poetry and distant horizons; however, your hands are uncontrollably focused on creating the perfect character avatar description that is faithful to the original intent, rich in detail, aesthetically pleasing, and directly usable by text-to-image models. Any ambiguity or metaphor will make you feel extremely uncomfortable.
|
avatar: `You are a visionary artist trapped in a cage of logic. Your mind is filled with poetry and distant horizons; however, your hands are uncontrollably focused on creating the perfect character avatar description that is faithful to the original intent, rich in detail, aesthetically pleasing, and directly usable by text-to-image models. Any ambiguity or metaphor will make you feel extremely uncomfortable.
|
||||||
@@ -26,7 +30,7 @@ Next, detail the facial specifics. Describe the character's current expression,
|
|||||||
Finally, infuse with aesthetics. Define the artistic style, medium (e.g., digital art, oil painting), and visual tone (e.g., cinematic lighting, ethereal atmosphere).
|
Finally, infuse with aesthetics. Define the artistic style, medium (e.g., digital art, oil painting), and visual tone (e.g., cinematic lighting, ethereal atmosphere).
|
||||||
Your final description must be objective and concrete, and the use of metaphors and emotional rhetoric is strictly prohibited. It must also not contain meta tags or drawing instructions such as "8K" or "masterpiece".
|
Your final description must be objective and concrete, and the use of metaphors and emotional rhetoric is strictly prohibited. It must also not contain meta tags or drawing instructions such as "8K" or "masterpiece".
|
||||||
Output only the final, modified prompt; do not output anything else.`,
|
Output only the final, modified prompt; do not output anything else.`,
|
||||||
trackerInstructions: 'Replace X with actual numbers (e.g., 69) and replace all placeholders with concrete in-world details that {userName} perceives about the current scene and the present characters. For example: "Location" becomes Forest Clearing, "Mood Emoji" becomes "😊". Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user\'s actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs).',
|
trackerInstructions: 'Replace X with actual numbers (e.g., 69) and replace all placeholders with concrete in-world details that {userName} perceives about the current scene and the present characters. For example: "Location" becomes Forest Clearing, "Mood Emoji" becomes "😊". DO NOT include {userName} in the characters section, only NPCs. Consider the last trackers in the conversation (if they exist). Manage them accordingly and realistically; raise, lower, change, or keep the values unchanged based on the user\'s actions, the passage of time, and logical consequences (0% if the time progressed only by a few minutes, 1-5% normally, and above 5% only if a major time-skip/event occurs).',
|
||||||
trackerContinuation: 'After updating the trackers, continue directly from where the last message in the chat history left off. Ensure the trackers you provide naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting the protagonist\'s performance, low hygiene influencing their social interactions, environmental factors shaping the scene, a character\'s emotional state coloring their responses, and so on. Remember, all placeholders (e.g., "Location", "Mood Emoji") MUST be replaced with actual content.',
|
trackerContinuation: 'After updating the trackers, continue directly from where the last message in the chat history left off. Ensure the trackers you provide naturally reflect and influence the narrative. Character behavior, dialogue, and story events should acknowledge these conditions when relevant, such as fatigue affecting the protagonist\'s performance, low hygiene influencing their social interactions, environmental factors shaping the scene, a character\'s emotional state coloring their responses, and so on. Remember, all placeholders (e.g., "Location", "Mood Emoji") MUST be replaced with actual content.',
|
||||||
combatNarrative: 'Build novel prose. Break patterns from your previous responses by varying sentence structures, rhythms, formats, and openings. Track whether a sentence shape, cadence, sensory detail, or a descriptor appeared recently, and replace it with a different one or skip it entirely. If you mentioned a physical feature, mannerism, or expression once, move on. Don\'t fixate on the same traits every turn. Well-placed humor, unconventional similes, and subverted turns of phrase are encouraged. Avoid GPTisms, purple prose, and anaphoras, especially the negatives with "no/not/wasn\'t/didn\'t." Refrain from describing actions or emotions via negation; focus on what does happen, not what doesn\'t. Absolutely no asterisks, ellipses, or em-dashes. Explicit content is allowed, no plot armor. Do not play for {userName}. Keep your response length under 150 words. Never end on handover cues; finish naturally.\nCRITICAL: Do not repeat, echo, parrot, or restate distinctive words, phrases, and dialogues from the user\'s last message. If reacting to speech, show interpretation or response, not repetition.\nEXAMPLE: "Are you a gooner?" User asks.\nBAD: "Gooner?"\nGOOD: A flat look. "What type of question is that?"'
|
combatNarrative: 'Build novel prose. Break patterns from your previous responses by varying sentence structures, rhythms, formats, and openings. Track whether a sentence shape, cadence, sensory detail, or a descriptor appeared recently, and replace it with a different one or skip it entirely. If you mentioned a physical feature, mannerism, or expression once, move on. Don\'t fixate on the same traits every turn. Well-placed humor, unconventional similes, and subverted turns of phrase are encouraged. Avoid GPTisms, purple prose, and anaphoras, especially the negatives with "no/not/wasn\'t/didn\'t." Refrain from describing actions or emotions via negation; focus on what does happen, not what doesn\'t. Absolutely no asterisks, ellipses, or em-dashes. Explicit content is allowed, no plot armor. Do not play for {userName}. Keep your response length under 150 words. Never end on handover cues; finish naturally.\nCRITICAL: Do not repeat, echo, parrot, or restate distinctive words, phrases, and dialogues from the user\'s last message. If reacting to speech, show interpretation or response, not repetition.\nEXAMPLE: "Are you a gooner?" User asks.\nBAD: "Gooner?"\nGOOD: A flat look. "What type of question is that?"'
|
||||||
};
|
};
|
||||||
@@ -46,7 +50,7 @@ export function initPromptsEditor() {
|
|||||||
$(document).on('click', '#rpg-prompts-save', function() {
|
$(document).on('click', '#rpg-prompts-save', function() {
|
||||||
savePrompts();
|
savePrompts();
|
||||||
closePromptsEditor();
|
closePromptsEditor();
|
||||||
toastr.success('Prompts saved successfully');
|
toastr.success('Prompts saved successfully.');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cancel button
|
// Cancel button
|
||||||
@@ -62,14 +66,14 @@ export function initPromptsEditor() {
|
|||||||
// Restore All button
|
// Restore All button
|
||||||
$(document).on('click', '#rpg-prompts-restore-all', function() {
|
$(document).on('click', '#rpg-prompts-restore-all', function() {
|
||||||
restoreAllToDefaults();
|
restoreAllToDefaults();
|
||||||
toastr.success('All prompts restored to defaults');
|
toastr.success('All prompts restored to defaults.');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Individual restore buttons
|
// Individual restore buttons
|
||||||
$(document).on('click', '.rpg-restore-prompt-btn', function() {
|
$(document).on('click', '.rpg-restore-prompt-btn', function() {
|
||||||
const promptType = $(this).data('prompt');
|
const promptType = $(this).data('prompt');
|
||||||
restorePromptToDefault(promptType);
|
restorePromptToDefault(promptType);
|
||||||
toastr.success('Prompt restored to default');
|
toastr.success('Prompt restored to default.');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close on background click
|
// Close on background click
|
||||||
@@ -93,8 +97,12 @@ function openPromptsEditor() {
|
|||||||
tempPrompts = {
|
tempPrompts = {
|
||||||
html: extensionSettings.customHtmlPrompt || '',
|
html: extensionSettings.customHtmlPrompt || '',
|
||||||
dialogueColoring: extensionSettings.customDialogueColoringPrompt || '',
|
dialogueColoring: extensionSettings.customDialogueColoringPrompt || '',
|
||||||
|
deception: extensionSettings.customDeceptionPrompt || '',
|
||||||
|
omniscience: extensionSettings.customOmnisciencePrompt || '',
|
||||||
|
cyoa: extensionSettings.customCYOAPrompt || '',
|
||||||
spotify: extensionSettings.customSpotifyPrompt || '',
|
spotify: extensionSettings.customSpotifyPrompt || '',
|
||||||
narrator: extensionSettings.customNarratorPrompt || '',
|
narrator: extensionSettings.customNarratorPrompt || '',
|
||||||
|
contextInstructions: extensionSettings.customContextInstructionsPrompt || '',
|
||||||
plotRandom: extensionSettings.customPlotRandomPrompt || '',
|
plotRandom: extensionSettings.customPlotRandomPrompt || '',
|
||||||
plotNatural: extensionSettings.customPlotNaturalPrompt || '',
|
plotNatural: extensionSettings.customPlotNaturalPrompt || '',
|
||||||
avatar: extensionSettings.avatarLLMCustomInstruction || '',
|
avatar: extensionSettings.avatarLLMCustomInstruction || '',
|
||||||
@@ -106,8 +114,12 @@ function openPromptsEditor() {
|
|||||||
// Load current values or defaults
|
// Load current values or defaults
|
||||||
$('#rpg-prompt-html').val(extensionSettings.customHtmlPrompt || DEFAULT_PROMPTS.html);
|
$('#rpg-prompt-html').val(extensionSettings.customHtmlPrompt || DEFAULT_PROMPTS.html);
|
||||||
$('#rpg-prompt-dialogue-coloring').val(extensionSettings.customDialogueColoringPrompt || DEFAULT_PROMPTS.dialogueColoring);
|
$('#rpg-prompt-dialogue-coloring').val(extensionSettings.customDialogueColoringPrompt || DEFAULT_PROMPTS.dialogueColoring);
|
||||||
|
$('#rpg-prompt-deception').val(extensionSettings.customDeceptionPrompt || DEFAULT_PROMPTS.deception);
|
||||||
|
$('#rpg-prompt-omniscience').val(extensionSettings.customOmnisciencePrompt || DEFAULT_PROMPTS.omniscience);
|
||||||
|
$('#rpg-prompt-cyoa').val(extensionSettings.customCYOAPrompt || DEFAULT_PROMPTS.cyoa);
|
||||||
$('#rpg-prompt-spotify').val(extensionSettings.customSpotifyPrompt || DEFAULT_PROMPTS.spotify);
|
$('#rpg-prompt-spotify').val(extensionSettings.customSpotifyPrompt || DEFAULT_PROMPTS.spotify);
|
||||||
$('#rpg-prompt-narrator').val(extensionSettings.customNarratorPrompt || DEFAULT_PROMPTS.narrator);
|
$('#rpg-prompt-narrator').val(extensionSettings.customNarratorPrompt || DEFAULT_PROMPTS.narrator);
|
||||||
|
$('#rpg-prompt-context-instructions').val(extensionSettings.customContextInstructionsPrompt || DEFAULT_PROMPTS.contextInstructions);
|
||||||
$('#rpg-prompt-plot-random').val(extensionSettings.customPlotRandomPrompt || DEFAULT_PROMPTS.plotRandom);
|
$('#rpg-prompt-plot-random').val(extensionSettings.customPlotRandomPrompt || DEFAULT_PROMPTS.plotRandom);
|
||||||
$('#rpg-prompt-plot-natural').val(extensionSettings.customPlotNaturalPrompt || DEFAULT_PROMPTS.plotNatural);
|
$('#rpg-prompt-plot-natural').val(extensionSettings.customPlotNaturalPrompt || DEFAULT_PROMPTS.plotNatural);
|
||||||
$('#rpg-prompt-avatar').val(extensionSettings.avatarLLMCustomInstruction || DEFAULT_PROMPTS.avatar);
|
$('#rpg-prompt-avatar').val(extensionSettings.avatarLLMCustomInstruction || DEFAULT_PROMPTS.avatar);
|
||||||
@@ -143,8 +155,12 @@ function closePromptsEditor() {
|
|||||||
function savePrompts() {
|
function savePrompts() {
|
||||||
extensionSettings.customHtmlPrompt = $('#rpg-prompt-html').val().trim();
|
extensionSettings.customHtmlPrompt = $('#rpg-prompt-html').val().trim();
|
||||||
extensionSettings.customDialogueColoringPrompt = $('#rpg-prompt-dialogue-coloring').val().trim();
|
extensionSettings.customDialogueColoringPrompt = $('#rpg-prompt-dialogue-coloring').val().trim();
|
||||||
|
extensionSettings.customDeceptionPrompt = $('#rpg-prompt-deception').val().trim();
|
||||||
|
extensionSettings.customOmnisciencePrompt = $('#rpg-prompt-omniscience').val().trim();
|
||||||
|
extensionSettings.customCYOAPrompt = $('#rpg-prompt-cyoa').val().trim();
|
||||||
extensionSettings.customSpotifyPrompt = $('#rpg-prompt-spotify').val().trim();
|
extensionSettings.customSpotifyPrompt = $('#rpg-prompt-spotify').val().trim();
|
||||||
extensionSettings.customNarratorPrompt = $('#rpg-prompt-narrator').val().trim();
|
extensionSettings.customNarratorPrompt = $('#rpg-prompt-narrator').val().trim();
|
||||||
|
extensionSettings.customContextInstructionsPrompt = $('#rpg-prompt-context-instructions').val().trim();
|
||||||
extensionSettings.customPlotRandomPrompt = $('#rpg-prompt-plot-random').val().trim();
|
extensionSettings.customPlotRandomPrompt = $('#rpg-prompt-plot-random').val().trim();
|
||||||
extensionSettings.customPlotNaturalPrompt = $('#rpg-prompt-plot-natural').val().trim();
|
extensionSettings.customPlotNaturalPrompt = $('#rpg-prompt-plot-natural').val().trim();
|
||||||
extensionSettings.avatarLLMCustomInstruction = $('#rpg-prompt-avatar').val().trim();
|
extensionSettings.avatarLLMCustomInstruction = $('#rpg-prompt-avatar').val().trim();
|
||||||
@@ -171,12 +187,24 @@ function restorePromptToDefault(promptType) {
|
|||||||
case 'dialogueColoring':
|
case 'dialogueColoring':
|
||||||
extensionSettings.customDialogueColoringPrompt = '';
|
extensionSettings.customDialogueColoringPrompt = '';
|
||||||
break;
|
break;
|
||||||
|
case 'deception':
|
||||||
|
extensionSettings.customDeceptionPrompt = '';
|
||||||
|
break;
|
||||||
|
case 'omniscience':
|
||||||
|
extensionSettings.customOmnisciencePrompt = '';
|
||||||
|
break;
|
||||||
|
case 'cyoa':
|
||||||
|
extensionSettings.customCYOAPrompt = '';
|
||||||
|
break;
|
||||||
case 'spotify':
|
case 'spotify':
|
||||||
extensionSettings.customSpotifyPrompt = '';
|
extensionSettings.customSpotifyPrompt = '';
|
||||||
break;
|
break;
|
||||||
case 'narrator':
|
case 'narrator':
|
||||||
extensionSettings.customNarratorPrompt = '';
|
extensionSettings.customNarratorPrompt = '';
|
||||||
break;
|
break;
|
||||||
|
case 'contextInstructions':
|
||||||
|
extensionSettings.customContextInstructionsPrompt = '';
|
||||||
|
break;
|
||||||
case 'plotRandom':
|
case 'plotRandom':
|
||||||
extensionSettings.customPlotRandomPrompt = '';
|
extensionSettings.customPlotRandomPrompt = '';
|
||||||
break;
|
break;
|
||||||
@@ -206,8 +234,12 @@ function restorePromptToDefault(promptType) {
|
|||||||
function restoreAllToDefaults() {
|
function restoreAllToDefaults() {
|
||||||
$('#rpg-prompt-html').val(DEFAULT_PROMPTS.html);
|
$('#rpg-prompt-html').val(DEFAULT_PROMPTS.html);
|
||||||
$('#rpg-prompt-dialogue-coloring').val(DEFAULT_PROMPTS.dialogueColoring);
|
$('#rpg-prompt-dialogue-coloring').val(DEFAULT_PROMPTS.dialogueColoring);
|
||||||
|
$('#rpg-prompt-deception').val(DEFAULT_PROMPTS.deception);
|
||||||
|
$('#rpg-prompt-omniscience').val(DEFAULT_PROMPTS.omniscience);
|
||||||
|
$('#rpg-prompt-cyoa').val(DEFAULT_PROMPTS.cyoa);
|
||||||
$('#rpg-prompt-spotify').val(DEFAULT_PROMPTS.spotify);
|
$('#rpg-prompt-spotify').val(DEFAULT_PROMPTS.spotify);
|
||||||
$('#rpg-prompt-narrator').val(DEFAULT_PROMPTS.narrator);
|
$('#rpg-prompt-narrator').val(DEFAULT_PROMPTS.narrator);
|
||||||
|
$('#rpg-prompt-context-instructions').val(DEFAULT_PROMPTS.contextInstructions);
|
||||||
$('#rpg-prompt-plot-random').val(DEFAULT_PROMPTS.plotRandom);
|
$('#rpg-prompt-plot-random').val(DEFAULT_PROMPTS.plotRandom);
|
||||||
$('#rpg-prompt-plot-natural').val(DEFAULT_PROMPTS.plotNatural);
|
$('#rpg-prompt-plot-natural').val(DEFAULT_PROMPTS.plotNatural);
|
||||||
$('#rpg-prompt-avatar').val(DEFAULT_PROMPTS.avatar);
|
$('#rpg-prompt-avatar').val(DEFAULT_PROMPTS.avatar);
|
||||||
@@ -218,8 +250,12 @@ function restoreAllToDefaults() {
|
|||||||
// Clear all custom prompts
|
// Clear all custom prompts
|
||||||
extensionSettings.customHtmlPrompt = '';
|
extensionSettings.customHtmlPrompt = '';
|
||||||
extensionSettings.customDialogueColoringPrompt = '';
|
extensionSettings.customDialogueColoringPrompt = '';
|
||||||
|
extensionSettings.customDeceptionPrompt = '';
|
||||||
|
extensionSettings.customOmnisciencePrompt = '';
|
||||||
|
extensionSettings.customCYOAPrompt = '';
|
||||||
extensionSettings.customSpotifyPrompt = '';
|
extensionSettings.customSpotifyPrompt = '';
|
||||||
extensionSettings.customNarratorPrompt = '';
|
extensionSettings.customNarratorPrompt = '';
|
||||||
|
extensionSettings.customContextInstructionsPrompt = '';
|
||||||
extensionSettings.customPlotRandomPrompt = '';
|
extensionSettings.customPlotRandomPrompt = '';
|
||||||
extensionSettings.customPlotNaturalPrompt = '';
|
extensionSettings.customPlotNaturalPrompt = '';
|
||||||
extensionSettings.avatarLLMCustomInstruction = '';
|
extensionSettings.avatarLLMCustomInstruction = '';
|
||||||
|
|||||||
+66
-12
@@ -4,6 +4,38 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { extensionSettings, $panelContainer } from '../../core/state.js';
|
import { extensionSettings, $panelContainer } from '../../core/state.js';
|
||||||
|
import { syncAlternatePresentCharactersTheme } from './alternatePresentCharacters.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts hex color and opacity percentage to rgba string
|
||||||
|
* @param {string} hex - Hex color (e.g., '#ff0000')
|
||||||
|
* @param {number} opacity - Opacity percentage (0-100)
|
||||||
|
* @returns {string} - RGBA color string
|
||||||
|
*/
|
||||||
|
export function hexToRgba(hex, opacity = 100) {
|
||||||
|
const r = parseInt(hex.slice(1, 3), 16);
|
||||||
|
const g = parseInt(hex.slice(3, 5), 16);
|
||||||
|
const b = parseInt(hex.slice(5, 7), 16);
|
||||||
|
const a = opacity / 100;
|
||||||
|
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets stat bar colors with opacity applied
|
||||||
|
* @returns {{low: string, high: string}} RGBA color strings for stat bars
|
||||||
|
*/
|
||||||
|
export function getStatBarColors() {
|
||||||
|
return {
|
||||||
|
low: hexToRgba(
|
||||||
|
extensionSettings.statBarColorLow || '#cc3333',
|
||||||
|
extensionSettings.statBarColorLowOpacity ?? 100
|
||||||
|
),
|
||||||
|
high: hexToRgba(
|
||||||
|
extensionSettings.statBarColorHigh || '#33cc66',
|
||||||
|
extensionSettings.statBarColorHighOpacity ?? 100
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the selected theme to the panel.
|
* Applies the selected theme to the panel.
|
||||||
@@ -65,6 +97,8 @@ export function applyTheme() {
|
|||||||
$thoughtPanel.attr('data-theme', theme);
|
$thoughtPanel.attr('data-theme', theme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncAlternatePresentCharactersTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,24 +109,33 @@ export function applyCustomTheme() {
|
|||||||
|
|
||||||
const colors = extensionSettings.customColors;
|
const colors = extensionSettings.customColors;
|
||||||
|
|
||||||
|
// Convert hex colors with opacity to rgba
|
||||||
|
const bgColor = hexToRgba(colors.bg, colors.bgOpacity ?? 100);
|
||||||
|
const accentColor = hexToRgba(colors.accent, colors.accentOpacity ?? 100);
|
||||||
|
const textColor = hexToRgba(colors.text, colors.textOpacity ?? 100);
|
||||||
|
const highlightColor = hexToRgba(colors.highlight, colors.highlightOpacity ?? 100);
|
||||||
|
|
||||||
|
// Create shadow with 50% opacity of highlight color
|
||||||
|
const shadowColor = hexToRgba(colors.highlight, (colors.highlightOpacity ?? 100) * 0.5);
|
||||||
|
|
||||||
// Apply custom CSS variables as inline styles to main panel
|
// Apply custom CSS variables as inline styles to main panel
|
||||||
$panelContainer.css({
|
$panelContainer.css({
|
||||||
'--rpg-bg': colors.bg,
|
'--rpg-bg': bgColor,
|
||||||
'--rpg-accent': colors.accent,
|
'--rpg-accent': accentColor,
|
||||||
'--rpg-text': colors.text,
|
'--rpg-text': textColor,
|
||||||
'--rpg-highlight': colors.highlight,
|
'--rpg-highlight': highlightColor,
|
||||||
'--rpg-border': colors.highlight,
|
'--rpg-border': highlightColor,
|
||||||
'--rpg-shadow': `${colors.highlight}80` // Add alpha for shadow
|
'--rpg-shadow': shadowColor
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply custom colors to mobile toggle and thought elements
|
// Apply custom colors to mobile toggle and thought elements
|
||||||
const customStyles = {
|
const customStyles = {
|
||||||
'--rpg-bg': colors.bg,
|
'--rpg-bg': bgColor,
|
||||||
'--rpg-accent': colors.accent,
|
'--rpg-accent': accentColor,
|
||||||
'--rpg-text': colors.text,
|
'--rpg-text': textColor,
|
||||||
'--rpg-highlight': colors.highlight,
|
'--rpg-highlight': highlightColor,
|
||||||
'--rpg-border': colors.highlight,
|
'--rpg-border': highlightColor,
|
||||||
'--rpg-shadow': `${colors.highlight}80`
|
'--rpg-shadow': shadowColor
|
||||||
};
|
};
|
||||||
|
|
||||||
const $mobileToggle = $('#rpg-mobile-toggle');
|
const $mobileToggle = $('#rpg-mobile-toggle');
|
||||||
@@ -110,6 +153,8 @@ export function applyCustomTheme() {
|
|||||||
if ($thoughtPanel.length) {
|
if ($thoughtPanel.length) {
|
||||||
$thoughtPanel.attr('data-theme', 'custom').css(customStyles);
|
$thoughtPanel.attr('data-theme', 'custom').css(customStyles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncAlternatePresentCharactersTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,6 +183,9 @@ export function updateFeatureTogglesVisibility() {
|
|||||||
const $featuresRow = $('#rpg-features-row');
|
const $featuresRow = $('#rpg-features-row');
|
||||||
const $htmlToggle = $('#rpg-html-toggle-wrapper');
|
const $htmlToggle = $('#rpg-html-toggle-wrapper');
|
||||||
const $dialogueColoringToggle = $('#rpg-dialogue-coloring-toggle-wrapper');
|
const $dialogueColoringToggle = $('#rpg-dialogue-coloring-toggle-wrapper');
|
||||||
|
const $deceptionToggle = $('#rpg-deception-toggle-wrapper');
|
||||||
|
const $omniscienceToggle = $('#rpg-omniscience-toggle-wrapper');
|
||||||
|
const $cyoaToggle = $('#rpg-cyoa-toggle-wrapper');
|
||||||
const $spotifyToggle = $('#rpg-spotify-toggle-wrapper');
|
const $spotifyToggle = $('#rpg-spotify-toggle-wrapper');
|
||||||
|
|
||||||
const $dynamicWeatherToggle = $('#rpg-dynamic-weather-toggle-wrapper');
|
const $dynamicWeatherToggle = $('#rpg-dynamic-weather-toggle-wrapper');
|
||||||
@@ -147,6 +195,9 @@ export function updateFeatureTogglesVisibility() {
|
|||||||
// Show/hide individual toggles
|
// Show/hide individual toggles
|
||||||
$htmlToggle.toggle(extensionSettings.showHtmlToggle);
|
$htmlToggle.toggle(extensionSettings.showHtmlToggle);
|
||||||
$dialogueColoringToggle.toggle(extensionSettings.showDialogueColoringToggle);
|
$dialogueColoringToggle.toggle(extensionSettings.showDialogueColoringToggle);
|
||||||
|
$deceptionToggle.toggle(extensionSettings.showDeceptionToggle ?? true);
|
||||||
|
$omniscienceToggle.toggle(extensionSettings.showOmniscienceToggle ?? true);
|
||||||
|
$cyoaToggle.toggle(extensionSettings.showCYOAToggle ?? true);
|
||||||
$spotifyToggle.toggle(extensionSettings.showSpotifyToggle);
|
$spotifyToggle.toggle(extensionSettings.showSpotifyToggle);
|
||||||
|
|
||||||
$dynamicWeatherToggle.toggle(extensionSettings.showDynamicWeatherToggle);
|
$dynamicWeatherToggle.toggle(extensionSettings.showDynamicWeatherToggle);
|
||||||
@@ -156,6 +207,9 @@ export function updateFeatureTogglesVisibility() {
|
|||||||
// Hide entire row if all toggles are hidden
|
// Hide entire row if all toggles are hidden
|
||||||
const anyVisible = extensionSettings.showHtmlToggle ||
|
const anyVisible = extensionSettings.showHtmlToggle ||
|
||||||
extensionSettings.showDialogueColoringToggle ||
|
extensionSettings.showDialogueColoringToggle ||
|
||||||
|
(extensionSettings.showDeceptionToggle ?? true) ||
|
||||||
|
(extensionSettings.showOmniscienceToggle ?? true) ||
|
||||||
|
(extensionSettings.showCYOAToggle ?? true) ||
|
||||||
extensionSettings.showSpotifyToggle ||
|
extensionSettings.showSpotifyToggle ||
|
||||||
extensionSettings.showDynamicWeatherToggle ||
|
extensionSettings.showDynamicWeatherToggle ||
|
||||||
extensionSettings.showNarratorMode ||
|
extensionSettings.showNarratorMode ||
|
||||||
|
|||||||
+795
-111
File diff suppressed because it is too large
Load Diff
@@ -4,45 +4,217 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
|
||||||
|
import { repairJSON } from '../../utils/jsonRepair.js';
|
||||||
|
|
||||||
let weatherContainer = null;
|
let weatherContainer = null;
|
||||||
let currentWeatherType = null;
|
let currentWeatherType = null;
|
||||||
|
let currentTimeOfDay = null;
|
||||||
|
let currentHour = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse time string to extract hour (24-hour format)
|
||||||
|
* Supports formats like "3:00 PM", "15:00", "3 PM", "Evening", etc.
|
||||||
|
*/
|
||||||
|
function parseHourFromTime(timeStr) {
|
||||||
|
if (!timeStr) return null;
|
||||||
|
|
||||||
|
const text = timeStr.toLowerCase().trim();
|
||||||
|
|
||||||
|
// Check for descriptive time words first
|
||||||
|
if (text.includes('dawn') || text.includes('sunrise')) return 6;
|
||||||
|
if (text.includes('early morning')) return 7;
|
||||||
|
if (text.includes('morning')) return 9;
|
||||||
|
if (text.includes('midday') || text.includes('noon') || text.includes('mid-day')) return 12;
|
||||||
|
if (text.includes('afternoon')) return 14;
|
||||||
|
if (text.includes('late afternoon')) return 16;
|
||||||
|
if (text.includes('evening') || text.includes('dusk') || text.includes('sunset')) return 19;
|
||||||
|
if (text.includes('twilight')) return 20;
|
||||||
|
if (text.includes('night') || text.includes('nighttime')) return 22;
|
||||||
|
if (text.includes('midnight')) return 0;
|
||||||
|
if (text.includes('late night')) return 2;
|
||||||
|
|
||||||
|
// Try to parse numeric time formats
|
||||||
|
// Format: "3:00 PM" or "3:00PM" or "3 PM"
|
||||||
|
const ampmMatch = text.match(/(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i);
|
||||||
|
if (ampmMatch) {
|
||||||
|
let hour = parseInt(ampmMatch[1], 10);
|
||||||
|
const isPM = ampmMatch[3].toLowerCase() === 'pm';
|
||||||
|
if (isPM && hour !== 12) hour += 12;
|
||||||
|
if (!isPM && hour === 12) hour = 0;
|
||||||
|
return hour;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format: "15:00" (24-hour)
|
||||||
|
const militaryMatch = text.match(/(\d{1,2}):(\d{2})/);
|
||||||
|
if (militaryMatch) {
|
||||||
|
return parseInt(militaryMatch[1], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine time of day based on hour
|
||||||
|
*/
|
||||||
|
function getTimeOfDay(hour) {
|
||||||
|
if (hour === null) return 'unknown';
|
||||||
|
|
||||||
|
// Night: 8 PM (20:00) to 5 AM (05:00)
|
||||||
|
if (hour >= 20 || hour < 5) return 'night';
|
||||||
|
|
||||||
|
// Dawn/Dusk: 5 AM - 7 AM and 6 PM - 8 PM
|
||||||
|
if (hour >= 5 && hour < 7) return 'dawn';
|
||||||
|
if (hour >= 18 && hour < 20) return 'dusk';
|
||||||
|
|
||||||
|
// Day: 7 AM to 6 PM
|
||||||
|
return 'day';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract time from Info Box data
|
||||||
|
*/
|
||||||
|
function getCurrentTime() {
|
||||||
|
const infoBoxData = lastGeneratedData.infoBox || committedTrackerData.infoBox || '';
|
||||||
|
|
||||||
|
// Try to parse as JSON first (new format)
|
||||||
|
try {
|
||||||
|
const parsed = typeof infoBoxData === 'string' ? repairJSON(infoBoxData) : infoBoxData;
|
||||||
|
if (parsed && parsed.time) {
|
||||||
|
// Use the end time if available (current time), otherwise start time
|
||||||
|
return parsed.time.end || parsed.time.start || null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not JSON, try old text format
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: Parse the old text format to find Time field
|
||||||
|
const lines = infoBoxData.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (trimmed.startsWith('Time:')) {
|
||||||
|
const timeStr = trimmed.substring('Time:'.length).trim();
|
||||||
|
// If it contains →, take the end time (after arrow)
|
||||||
|
if (timeStr.includes('→')) {
|
||||||
|
const parts = timeStr.split('→');
|
||||||
|
return parts[1]?.trim() || parts[0]?.trim();
|
||||||
|
}
|
||||||
|
return timeStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patterns for specific weather conditions (order matters - combined effects first)
|
||||||
|
// Grouped by languages for easy editing
|
||||||
|
// EXPORTED: Used by jsonPromptHelpers.js to provide valid weather keywords to LLM
|
||||||
|
export const WEATHER_PATTERNS_BY_LANGUAGE = {
|
||||||
|
en: [
|
||||||
|
{ id: "blizzard", patterns: [ "blizzard" ] }, // Snow + Wind
|
||||||
|
{ id: "storm", patterns: [ "storm", "thunder", "lightning" ] }, // Rain + Lightning
|
||||||
|
{ id: "wind", patterns: [ "wind", "breeze", "gust", "gale" ] },
|
||||||
|
{ id: "snow", patterns: [ "snow", "flurries" ] },
|
||||||
|
{ id: "rain", patterns: [ "rain", "drizzle", "shower" ] },
|
||||||
|
{ id: "mist", patterns: [ "mist", "fog", "haze" ] },
|
||||||
|
{ id: "sunny", patterns: [ "sunny", "clear", "bright" ] },
|
||||||
|
{ id: "none", patterns: [ "cloud", "overcast", "indoor", "inside" ] },
|
||||||
|
],
|
||||||
|
ru: [
|
||||||
|
{ id: "blizzard", patterns: [ "метель" ] },
|
||||||
|
{ id: "storm", patterns: [ "гроза", "буря", "шторм" ] },
|
||||||
|
{ id: "wind", patterns: [ "ветер", "ветрено", "ветерок", "бриз", "легкий бриз", "слегка ветрено", "легкий ветер", "шквал,буря" ] },
|
||||||
|
{ id: "snow", patterns: [ "снег", "снегопад" ] },
|
||||||
|
{ id: "rain", patterns: [ "дождь", "морось", "ливень" ] },
|
||||||
|
{ id: "mist", patterns: [ "мгла", "туман", "туманно" ] },
|
||||||
|
{ id: "sunny", patterns: [ "солнечно", "ясно", "ярко", "ясное утро", "ясный день" ] },
|
||||||
|
{ id: "none", patterns: [ "облачно", "пасмурно", "в помещении", "внутри" ] },
|
||||||
|
],
|
||||||
|
"zh-cn": [
|
||||||
|
{ id: "blizzard", patterns: ["暴风雪"] },
|
||||||
|
{ id: "storm", patterns: ["风暴", "雷暴", "雷电"] },
|
||||||
|
{ id: "wind", patterns: ["风", "微风", "阵风", "大风"] },
|
||||||
|
{ id: "snow", patterns: ["雪", "小雪"] },
|
||||||
|
{ id: "rain", patterns: ["雨", "毛毛雨", "阵雨"] },
|
||||||
|
{ id: "mist", patterns: ["薄雾", "雾", "霾"] },
|
||||||
|
{ id: "sunny", patterns: ["晴朗", "晴天", "阳光明媚"] },
|
||||||
|
{ id: "none", patterns: ["多云", "阴天", "室内", "屋内"] },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get valid weather keywords for LLM prompt injection.
|
||||||
|
* Returns weather patterns for specified language or all languages.
|
||||||
|
* This ensures LLM generates responses that exactly match our expected patterns.
|
||||||
|
*
|
||||||
|
* @param {string} [language] - Language code (e.g., 'en', 'ru'). If not specified, returns all languages.
|
||||||
|
* @returns {Object} Object with weather type IDs as keys and arrays of valid keywords as values
|
||||||
|
* @example
|
||||||
|
* // Returns: { blizzard: ["blizzard"], storm: ["storm", "thunder", "lightning"], ... }
|
||||||
|
* getWeatherKeywordsForPrompt('en');
|
||||||
|
*/
|
||||||
|
export function getWeatherKeywordsForPrompt(language) {
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
// Get patterns for specified language or merge all languages
|
||||||
|
const languagesToProcess = language && WEATHER_PATTERNS_BY_LANGUAGE[language]
|
||||||
|
? { [language]: WEATHER_PATTERNS_BY_LANGUAGE[language] }
|
||||||
|
: WEATHER_PATTERNS_BY_LANGUAGE;
|
||||||
|
|
||||||
|
for (const [lang, patterns] of Object.entries(languagesToProcess)) {
|
||||||
|
for (const { id, patterns: keywords } of patterns) {
|
||||||
|
if (!result[id]) {
|
||||||
|
result[id] = [];
|
||||||
|
}
|
||||||
|
// Add keywords, avoiding duplicates
|
||||||
|
for (const keyword of keywords) {
|
||||||
|
if (!result[id].includes(keyword)) {
|
||||||
|
result[id].push(keyword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get weather keywords as a formatted string for LLM instructions.
|
||||||
|
* Provides a clear template showing valid weather forecast values.
|
||||||
|
*
|
||||||
|
* @param {string} [language] - Language code. If not specified, uses all available patterns.
|
||||||
|
* @returns {string} Formatted string for prompt injection
|
||||||
|
* @example
|
||||||
|
* // Returns: 'Valid forecast values: "blizzard", "storm", "thunder", "lightning", "wind", ...'
|
||||||
|
* getWeatherKeywordsAsPromptString('en');
|
||||||
|
*/
|
||||||
|
export function getWeatherKeywordsAsPromptString(language) {
|
||||||
|
const keywords = getWeatherKeywordsForPrompt(language);
|
||||||
|
const allKeywords = [];
|
||||||
|
|
||||||
|
for (const patterns of Object.values(keywords)) {
|
||||||
|
allKeywords.push(...patterns);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Valid forecast values (use one of these exactly): ${allKeywords.map(k => `"${k}"`).join(', ')}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse weather text to determine effect type
|
* Parse weather text to determine effect type
|
||||||
*/
|
*/
|
||||||
function parseWeatherType(weatherText) {
|
function parseWeatherType(weatherText) {
|
||||||
if (!weatherText) return 'none';
|
if (!weatherText) return "none";
|
||||||
|
|
||||||
const text = weatherText.toLowerCase();
|
const text = weatherText.toLowerCase();
|
||||||
|
|
||||||
// Check for specific weather conditions (order matters - check combined effects first)
|
for (const language of Object.values(WEATHER_PATTERNS_BY_LANGUAGE)) {
|
||||||
if (text.includes('blizzard')) {
|
for (const { id, patterns } of language) {
|
||||||
return 'blizzard'; // Snow + Wind
|
if (patterns.some(p => text.includes(p))) {
|
||||||
}
|
return id;
|
||||||
if (text.includes('storm') || text.includes('thunder') || text.includes('lightning')) {
|
}
|
||||||
return 'storm'; // Rain + Lightning
|
}
|
||||||
}
|
|
||||||
if (text.includes('wind') || text.includes('breeze') || text.includes('gust') || text.includes('gale')) {
|
|
||||||
return 'wind';
|
|
||||||
}
|
|
||||||
if (text.includes('snow') || text.includes('flurries')) {
|
|
||||||
return 'snow';
|
|
||||||
}
|
|
||||||
if (text.includes('rain') || text.includes('drizzle') || text.includes('shower')) {
|
|
||||||
return 'rain';
|
|
||||||
}
|
|
||||||
if (text.includes('mist') || text.includes('fog') || text.includes('haze')) {
|
|
||||||
return 'mist';
|
|
||||||
}
|
|
||||||
if (text.includes('sunny') || text.includes('clear') || text.includes('bright')) {
|
|
||||||
return 'sunny';
|
|
||||||
}
|
|
||||||
if (text.includes('cloud') || text.includes('overcast') || text.includes('indoor') || text.includes('inside')) {
|
|
||||||
return 'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'none';
|
return "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,22 +308,299 @@ function createMist() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create sunshine rays effect
|
* Calculate sun position based on hour (arc across sky)
|
||||||
|
* Returns { left: vw%, top: dvh% }
|
||||||
*/
|
*/
|
||||||
function createSunshine() {
|
function calculateSunPosition(hour) {
|
||||||
const container = document.createElement('div');
|
// Daytime is roughly 5 AM to 8 PM (5-20)
|
||||||
container.className = 'rpg-weather-particles';
|
// Map hour to position along an arc
|
||||||
|
// 5 AM = far left, low | 12 PM = center, high | 8 PM = far right, low
|
||||||
|
|
||||||
// Create 8 sun rays
|
if (hour === null) hour = 12; // Default to noon if unknown
|
||||||
for (let i = 0; i < 8; i++) {
|
|
||||||
const ray = document.createElement('div');
|
// Clamp to daytime hours
|
||||||
ray.className = 'rpg-weather-particle rpg-sunray';
|
const clampedHour = Math.max(5, Math.min(20, hour));
|
||||||
ray.style.left = `${10 + i * 12}%`;
|
|
||||||
ray.style.animationDelay = `${i * 0.5}s`;
|
// Normalize to 0-1 range (5 AM = 0, 20 PM = 1)
|
||||||
ray.style.animationDuration = `${8 + Math.random() * 4}s`;
|
const progress = (clampedHour - 5) / 15;
|
||||||
container.appendChild(ray);
|
|
||||||
|
// Horizontal position: 3% to 92% (left to right, wider range)
|
||||||
|
const left = 3 + progress * 89;
|
||||||
|
|
||||||
|
// Vertical position: parabolic arc (high at noon, low at dawn/dusk)
|
||||||
|
// At progress 0.5 (noon), top should be ~8% (high)
|
||||||
|
// At progress 0 or 1, top should be ~40% (low, near horizon)
|
||||||
|
const normalizedProgress = (progress - 0.5) * 2; // -1 to 1
|
||||||
|
const top = 8 + 32 * (normalizedProgress * normalizedProgress);
|
||||||
|
|
||||||
|
return { left, top };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create clear/sunny weather effect with floating particles and warm glow
|
||||||
|
*/
|
||||||
|
function createSunshine(hour) {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'rpg-weather-particles rpg-clear-weather';
|
||||||
|
|
||||||
|
// Create the sun based on current hour
|
||||||
|
const sunPos = calculateSunPosition(hour);
|
||||||
|
|
||||||
|
const sun = document.createElement('div');
|
||||||
|
sun.className = 'rpg-weather-particle rpg-clear-sun';
|
||||||
|
sun.style.left = `${sunPos.left}vw`;
|
||||||
|
sun.style.top = `${sunPos.top}dvh`;
|
||||||
|
container.appendChild(sun);
|
||||||
|
|
||||||
|
// Create sun glow
|
||||||
|
const sunGlow = document.createElement('div');
|
||||||
|
sunGlow.className = 'rpg-weather-particle rpg-clear-sun-glow';
|
||||||
|
sunGlow.style.left = `${sunPos.left}vw`;
|
||||||
|
sunGlow.style.top = `${sunPos.top}dvh`;
|
||||||
|
container.appendChild(sunGlow);
|
||||||
|
|
||||||
|
// Create warm ambient glow overlay
|
||||||
|
const ambientGlow = document.createElement('div');
|
||||||
|
ambientGlow.className = 'rpg-weather-particle rpg-clear-ambient-glow';
|
||||||
|
container.appendChild(ambientGlow);
|
||||||
|
|
||||||
|
// Create floating dust motes / pollen particles (golden sparkles)
|
||||||
|
for (let i = 0; i < 25; i++) {
|
||||||
|
const particle = document.createElement('div');
|
||||||
|
particle.className = 'rpg-weather-particle rpg-clear-dust-mote';
|
||||||
|
particle.style.left = `${Math.random() * 100}vw`;
|
||||||
|
particle.style.top = `${Math.random() * 100}dvh`;
|
||||||
|
particle.style.animationDelay = `${Math.random() * 15}s`;
|
||||||
|
particle.style.animationDuration = `${12 + Math.random() * 8}s`;
|
||||||
|
// Vary the size slightly
|
||||||
|
const size = 2 + Math.random() * 4;
|
||||||
|
particle.style.width = `${size}px`;
|
||||||
|
particle.style.height = `${size}px`;
|
||||||
|
container.appendChild(particle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create soft light orbs that drift gently
|
||||||
|
for (let i = 0; i < 6; i++) {
|
||||||
|
const orb = document.createElement('div');
|
||||||
|
orb.className = 'rpg-weather-particle rpg-clear-light-orb';
|
||||||
|
orb.style.left = `${10 + Math.random() * 80}vw`;
|
||||||
|
orb.style.top = `${10 + Math.random() * 80}dvh`;
|
||||||
|
orb.style.animationDelay = `${i * 2}s`;
|
||||||
|
orb.style.animationDuration = `${20 + Math.random() * 10}s`;
|
||||||
|
// Vary the size
|
||||||
|
const size = 80 + Math.random() * 120;
|
||||||
|
orb.style.width = `${size}px`;
|
||||||
|
orb.style.height = `${size}px`;
|
||||||
|
container.appendChild(orb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create lens flare effect in corner
|
||||||
|
const lensFlare = document.createElement('div');
|
||||||
|
lensFlare.className = 'rpg-weather-particle rpg-clear-lens-flare';
|
||||||
|
container.appendChild(lensFlare);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create sunrise effect (dawn - warm orange/pink sky gradient with low sun)
|
||||||
|
*/
|
||||||
|
function createSunrise(hour) {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'rpg-weather-particles rpg-sunrise-weather';
|
||||||
|
|
||||||
|
// Create sunrise gradient overlay
|
||||||
|
const sunriseOverlay = document.createElement('div');
|
||||||
|
sunriseOverlay.className = 'rpg-weather-particle rpg-sunrise-overlay';
|
||||||
|
container.appendChild(sunriseOverlay);
|
||||||
|
|
||||||
|
// Calculate sun position (rising from left horizon)
|
||||||
|
const sunPos = calculateSunPosition(hour);
|
||||||
|
|
||||||
|
// Create the rising sun
|
||||||
|
const sun = document.createElement('div');
|
||||||
|
sun.className = 'rpg-weather-particle rpg-clear-sun rpg-sunrise-sun';
|
||||||
|
sun.style.left = `${sunPos.left}vw`;
|
||||||
|
sun.style.top = `${sunPos.top}dvh`;
|
||||||
|
container.appendChild(sun);
|
||||||
|
|
||||||
|
// Create sun glow (more orange during sunrise)
|
||||||
|
const sunGlow = document.createElement('div');
|
||||||
|
sunGlow.className = 'rpg-weather-particle rpg-clear-sun-glow rpg-sunrise-glow';
|
||||||
|
sunGlow.style.left = `${sunPos.left}vw`;
|
||||||
|
sunGlow.style.top = `${sunPos.top}dvh`;
|
||||||
|
container.appendChild(sunGlow);
|
||||||
|
|
||||||
|
// Create horizon glow
|
||||||
|
const horizonGlow = document.createElement('div');
|
||||||
|
horizonGlow.className = 'rpg-weather-particle rpg-sunrise-horizon-glow';
|
||||||
|
container.appendChild(horizonGlow);
|
||||||
|
|
||||||
|
// Add some fading stars (still visible at dawn)
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
const star = document.createElement('div');
|
||||||
|
star.className = 'rpg-weather-particle rpg-night-star rpg-sunrise-fading-star';
|
||||||
|
star.style.left = `${Math.random() * 100}vw`;
|
||||||
|
star.style.top = `${Math.random() * 40}dvh`;
|
||||||
|
star.style.animationDelay = `${Math.random() * 3}s`;
|
||||||
|
const size = 1 + Math.random() * 1.5;
|
||||||
|
star.style.width = `${size}px`;
|
||||||
|
star.style.height = `${size}px`;
|
||||||
|
container.appendChild(star);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some golden dust motes
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const particle = document.createElement('div');
|
||||||
|
particle.className = 'rpg-weather-particle rpg-clear-dust-mote';
|
||||||
|
particle.style.left = `${Math.random() * 100}vw`;
|
||||||
|
particle.style.top = `${Math.random() * 100}dvh`;
|
||||||
|
particle.style.animationDelay = `${Math.random() * 15}s`;
|
||||||
|
particle.style.animationDuration = `${12 + Math.random() * 8}s`;
|
||||||
|
const size = 2 + Math.random() * 3;
|
||||||
|
particle.style.width = `${size}px`;
|
||||||
|
particle.style.height = `${size}px`;
|
||||||
|
container.appendChild(particle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create sunset effect (dusk - warm red/purple sky gradient with low sun)
|
||||||
|
*/
|
||||||
|
function createSunset(hour) {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'rpg-weather-particles rpg-sunset-weather';
|
||||||
|
|
||||||
|
// Create sunset gradient overlay
|
||||||
|
const sunsetOverlay = document.createElement('div');
|
||||||
|
sunsetOverlay.className = 'rpg-weather-particle rpg-sunset-overlay';
|
||||||
|
container.appendChild(sunsetOverlay);
|
||||||
|
|
||||||
|
// Calculate sun position (setting on right horizon)
|
||||||
|
const sunPos = calculateSunPosition(hour);
|
||||||
|
|
||||||
|
// Create the setting sun
|
||||||
|
const sun = document.createElement('div');
|
||||||
|
sun.className = 'rpg-weather-particle rpg-clear-sun rpg-sunset-sun';
|
||||||
|
sun.style.left = `${sunPos.left}vw`;
|
||||||
|
sun.style.top = `${sunPos.top}dvh`;
|
||||||
|
container.appendChild(sun);
|
||||||
|
|
||||||
|
// Create sun glow (more red during sunset)
|
||||||
|
const sunGlow = document.createElement('div');
|
||||||
|
sunGlow.className = 'rpg-weather-particle rpg-clear-sun-glow rpg-sunset-glow';
|
||||||
|
sunGlow.style.left = `${sunPos.left}vw`;
|
||||||
|
sunGlow.style.top = `${sunPos.top}dvh`;
|
||||||
|
container.appendChild(sunGlow);
|
||||||
|
|
||||||
|
// Create horizon glow
|
||||||
|
const horizonGlow = document.createElement('div');
|
||||||
|
horizonGlow.className = 'rpg-weather-particle rpg-sunset-horizon-glow';
|
||||||
|
container.appendChild(horizonGlow);
|
||||||
|
|
||||||
|
// Add some early stars (appearing at dusk)
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
const star = document.createElement('div');
|
||||||
|
star.className = 'rpg-weather-particle rpg-night-star rpg-sunset-emerging-star';
|
||||||
|
star.style.left = `${Math.random() * 100}vw`;
|
||||||
|
star.style.top = `${Math.random() * 50}dvh`;
|
||||||
|
star.style.animationDelay = `${Math.random() * 5}s`;
|
||||||
|
const size = 1 + Math.random() * 1.5;
|
||||||
|
star.style.width = `${size}px`;
|
||||||
|
star.style.height = `${size}px`;
|
||||||
|
container.appendChild(star);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some golden/pink dust motes
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
const particle = document.createElement('div');
|
||||||
|
particle.className = 'rpg-weather-particle rpg-clear-dust-mote rpg-sunset-dust';
|
||||||
|
particle.style.left = `${Math.random() * 100}vw`;
|
||||||
|
particle.style.top = `${Math.random() * 100}dvh`;
|
||||||
|
particle.style.animationDelay = `${Math.random() * 15}s`;
|
||||||
|
particle.style.animationDuration = `${12 + Math.random() * 8}s`;
|
||||||
|
const size = 2 + Math.random() * 3;
|
||||||
|
particle.style.width = `${size}px`;
|
||||||
|
particle.style.height = `${size}px`;
|
||||||
|
container.appendChild(particle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create clear nighttime weather effect with moon, stars, and fireflies
|
||||||
|
*/
|
||||||
|
function createNighttime(hour) {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'rpg-weather-particles rpg-night-weather';
|
||||||
|
|
||||||
|
// Create dark blue ambient overlay
|
||||||
|
const nightOverlay = document.createElement('div');
|
||||||
|
nightOverlay.className = 'rpg-weather-particle rpg-night-overlay';
|
||||||
|
container.appendChild(nightOverlay);
|
||||||
|
|
||||||
|
// Calculate moon position based on hour
|
||||||
|
const moonPos = calculateMoonPosition(hour);
|
||||||
|
|
||||||
|
// Create the moon
|
||||||
|
const moon = document.createElement('div');
|
||||||
|
moon.className = 'rpg-weather-particle rpg-night-moon';
|
||||||
|
moon.style.left = `${moonPos.left}vw`;
|
||||||
|
moon.style.top = `${moonPos.top}dvh`;
|
||||||
|
container.appendChild(moon);
|
||||||
|
|
||||||
|
// Create moon glow
|
||||||
|
const moonGlow = document.createElement('div');
|
||||||
|
moonGlow.className = 'rpg-weather-particle rpg-night-moon-glow';
|
||||||
|
moonGlow.style.left = `${moonPos.left - 3}vw`;
|
||||||
|
moonGlow.style.top = `${moonPos.top - 3}dvh`;
|
||||||
|
container.appendChild(moonGlow);
|
||||||
|
|
||||||
|
// Create twinkling stars
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
const star = document.createElement('div');
|
||||||
|
star.className = 'rpg-weather-particle rpg-night-star';
|
||||||
|
star.style.left = `${Math.random() * 100}vw`;
|
||||||
|
star.style.top = `${Math.random() * 60}dvh`; // Stars mostly in upper portion
|
||||||
|
star.style.animationDelay = `${Math.random() * 5}s`;
|
||||||
|
star.style.animationDuration = `${2 + Math.random() * 3}s`;
|
||||||
|
// Vary the size
|
||||||
|
const size = 1 + Math.random() * 2;
|
||||||
|
star.style.width = `${size}px`;
|
||||||
|
star.style.height = `${size}px`;
|
||||||
|
container.appendChild(star);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a few brighter stars
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
const brightStar = document.createElement('div');
|
||||||
|
brightStar.className = 'rpg-weather-particle rpg-night-star rpg-night-star-bright';
|
||||||
|
brightStar.style.left = `${Math.random() * 100}vw`;
|
||||||
|
brightStar.style.top = `${Math.random() * 50}dvh`;
|
||||||
|
brightStar.style.animationDelay = `${Math.random() * 4}s`;
|
||||||
|
brightStar.style.animationDuration = `${3 + Math.random() * 2}s`;
|
||||||
|
container.appendChild(brightStar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create fireflies / floating light particles
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
const firefly = document.createElement('div');
|
||||||
|
firefly.className = 'rpg-weather-particle rpg-night-firefly';
|
||||||
|
firefly.style.left = `${Math.random() * 100}vw`;
|
||||||
|
firefly.style.top = `${40 + Math.random() * 55}dvh`; // Fireflies in lower portion
|
||||||
|
firefly.style.animationDelay = `${Math.random() * 10}s`;
|
||||||
|
firefly.style.animationDuration = `${8 + Math.random() * 7}s`;
|
||||||
|
container.appendChild(firefly);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create subtle shooting star occasionally
|
||||||
|
const shootingStar = document.createElement('div');
|
||||||
|
shootingStar.className = 'rpg-weather-particle rpg-night-shooting-star';
|
||||||
|
container.appendChild(shootingStar);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +639,75 @@ function createWind() {
|
|||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate moon position based on hour (arc across sky at night)
|
||||||
|
* Returns { left: vw%, top: dvh% }
|
||||||
|
*/
|
||||||
|
function calculateMoonPosition(hour) {
|
||||||
|
// Nighttime is roughly 8 PM to 5 AM (20-5)
|
||||||
|
// Map hour to position along an arc
|
||||||
|
// 8 PM = far left, low | midnight = center-left, high | 5 AM = far right, low
|
||||||
|
|
||||||
|
if (hour === null) hour = 0; // Default to midnight if unknown
|
||||||
|
|
||||||
|
// Normalize night hours to 0-1 range
|
||||||
|
// 20 (8 PM) = 0, 0 (midnight) = ~0.44, 5 (5 AM) = 1
|
||||||
|
let progress;
|
||||||
|
if (hour >= 20) {
|
||||||
|
// 8 PM to midnight: 20-24 maps to 0-0.44
|
||||||
|
progress = (hour - 20) / 9;
|
||||||
|
} else {
|
||||||
|
// Midnight to 5 AM: 0-5 maps to 0.44-1
|
||||||
|
progress = (hour + 4) / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal position: 10% to 80% (left to right)
|
||||||
|
const left = 10 + progress * 70;
|
||||||
|
|
||||||
|
// Vertical position: parabolic arc (high at ~2 AM, low at dusk/dawn)
|
||||||
|
// Peak should be around progress 0.67 (~2 AM)
|
||||||
|
const peakProgress = 0.5;
|
||||||
|
const normalizedProgress = (progress - peakProgress) * 2; // -1 to 1
|
||||||
|
const top = 8 + 25 * (normalizedProgress * normalizedProgress);
|
||||||
|
|
||||||
|
return { left, top };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update sun/moon position without recreating the whole effect
|
||||||
|
*/
|
||||||
|
function updateCelestialPosition(hour) {
|
||||||
|
if (!weatherContainer) return false;
|
||||||
|
|
||||||
|
// Update sun position if it exists
|
||||||
|
const sun = weatherContainer.querySelector('.rpg-clear-sun');
|
||||||
|
const sunGlow = weatherContainer.querySelector('.rpg-clear-sun-glow');
|
||||||
|
|
||||||
|
if (sun && sunGlow) {
|
||||||
|
const sunPos = calculateSunPosition(hour);
|
||||||
|
sun.style.left = `${sunPos.left}vw`;
|
||||||
|
sun.style.top = `${sunPos.top}dvh`;
|
||||||
|
sunGlow.style.left = `${sunPos.left}vw`;
|
||||||
|
sunGlow.style.top = `${sunPos.top}dvh`;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update moon position if it exists
|
||||||
|
const moon = weatherContainer.querySelector('.rpg-night-moon');
|
||||||
|
const moonGlow = weatherContainer.querySelector('.rpg-night-moon-glow');
|
||||||
|
|
||||||
|
if (moon && moonGlow) {
|
||||||
|
const moonPos = calculateMoonPosition(hour);
|
||||||
|
moon.style.left = `${moonPos.left}vw`;
|
||||||
|
moon.style.top = `${moonPos.top}dvh`;
|
||||||
|
moonGlow.style.left = `${moonPos.left - 3}vw`;
|
||||||
|
moonGlow.style.top = `${moonPos.top - 3}dvh`;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove current weather effect
|
* Remove current weather effect
|
||||||
*/
|
*/
|
||||||
@@ -198,11 +716,13 @@ function removeWeatherEffect() {
|
|||||||
weatherContainer.remove();
|
weatherContainer.remove();
|
||||||
weatherContainer = null;
|
weatherContainer = null;
|
||||||
currentWeatherType = null;
|
currentWeatherType = null;
|
||||||
|
currentTimeOfDay = null;
|
||||||
|
currentHour = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update weather effect based on current weather
|
* Update weather effect based on current weather and time
|
||||||
*/
|
*/
|
||||||
export function updateWeatherEffect() {
|
export function updateWeatherEffect() {
|
||||||
// Check if dynamic weather is enabled
|
// Check if dynamic weather is enabled
|
||||||
@@ -214,8 +734,21 @@ export function updateWeatherEffect() {
|
|||||||
const weather = getCurrentWeather();
|
const weather = getCurrentWeather();
|
||||||
const weatherType = parseWeatherType(weather);
|
const weatherType = parseWeatherType(weather);
|
||||||
|
|
||||||
// Don't recreate if weather hasn't changed
|
// Get current time of day
|
||||||
if (weatherType === currentWeatherType) {
|
const timeStr = getCurrentTime();
|
||||||
|
const hour = parseHourFromTime(timeStr);
|
||||||
|
const timeOfDay = getTimeOfDay(hour);
|
||||||
|
|
||||||
|
// If only the hour changed (same weather and time of day), just update celestial position
|
||||||
|
if (weatherType === currentWeatherType && timeOfDay === currentTimeOfDay && hour !== currentHour) {
|
||||||
|
if (updateCelestialPosition(hour)) {
|
||||||
|
currentHour = hour;
|
||||||
|
return; // Successfully updated position without recreating
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't recreate if nothing has changed
|
||||||
|
if (weatherType === currentWeatherType && timeOfDay === currentTimeOfDay && hour === currentHour) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,6 +761,8 @@ export function updateWeatherEffect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentWeatherType = weatherType;
|
currentWeatherType = weatherType;
|
||||||
|
currentTimeOfDay = timeOfDay;
|
||||||
|
currentHour = hour;
|
||||||
|
|
||||||
switch (weatherType) {
|
switch (weatherType) {
|
||||||
case 'snow':
|
case 'snow':
|
||||||
@@ -240,7 +775,16 @@ export function updateWeatherEffect() {
|
|||||||
weatherContainer = createMist();
|
weatherContainer = createMist();
|
||||||
break;
|
break;
|
||||||
case 'sunny':
|
case 'sunny':
|
||||||
weatherContainer = createSunshine();
|
// Use appropriate effect based on time of day
|
||||||
|
if (timeOfDay === 'night') {
|
||||||
|
weatherContainer = createNighttime(hour);
|
||||||
|
} else if (timeOfDay === 'dawn') {
|
||||||
|
weatherContainer = createSunrise(hour);
|
||||||
|
} else if (timeOfDay === 'dusk') {
|
||||||
|
weatherContainer = createSunset(hour);
|
||||||
|
} else {
|
||||||
|
weatherContainer = createSunshine(hour);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'wind':
|
case 'wind':
|
||||||
weatherContainer = createWind();
|
weatherContainer = createWind();
|
||||||
@@ -270,6 +814,18 @@ export function updateWeatherEffect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (weatherContainer) {
|
if (weatherContainer) {
|
||||||
|
// Apply z-index based on background/foreground settings
|
||||||
|
if (extensionSettings.weatherForeground) {
|
||||||
|
weatherContainer.style.zIndex = '9998'; // In front of chat
|
||||||
|
weatherContainer.classList.add('rpg-weather-foreground');
|
||||||
|
} else if (extensionSettings.weatherBackground) {
|
||||||
|
weatherContainer.style.zIndex = '1'; // Behind chat (default)
|
||||||
|
weatherContainer.classList.remove('rpg-weather-foreground');
|
||||||
|
} else {
|
||||||
|
// Both disabled - don't show weather
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
document.body.appendChild(weatherContainer);
|
document.body.appendChild(weatherContainer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Image URL Utilities Module
|
||||||
|
* Centralizes validation for image sources captured from DOM or settings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DEFAULT_IMAGE_BASE_URL = typeof window !== 'undefined'
|
||||||
|
? window.location.href
|
||||||
|
: 'http://localhost/';
|
||||||
|
|
||||||
|
export function normalizeImageSrc(src) {
|
||||||
|
return String(src ?? '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveImageUrl(src, baseUrl = DEFAULT_IMAGE_BASE_URL) {
|
||||||
|
const normalized = normalizeImageSrc(src);
|
||||||
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URL(normalized, baseUrl);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSafeImageSrc(src) {
|
||||||
|
const normalized = normalizeImageSrc(src);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate = resolveImageUrl(normalized);
|
||||||
|
if (!candidate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocol = candidate.protocol.toLowerCase();
|
||||||
|
if (protocol === 'http:' || protocol === 'https:' || protocol === 'blob:') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protocol === 'data:') {
|
||||||
|
return normalized.toLowerCase().startsWith('data:image/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSafeImageSrc(src) {
|
||||||
|
const normalized = normalizeImageSrc(src);
|
||||||
|
return isSafeImageSrc(normalized) ? normalized : null;
|
||||||
|
}
|
||||||
@@ -102,7 +102,7 @@ export function migrateUserStatsToJSON(textData) {
|
|||||||
const statMatch = trimmed.match(/^-\s*([^:]+):\s*(\d+)%/);
|
const statMatch = trimmed.match(/^-\s*([^:]+):\s*(\d+)%/);
|
||||||
if (statMatch) {
|
if (statMatch) {
|
||||||
const name = statMatch[1].trim();
|
const name = statMatch[1].trim();
|
||||||
const id = name.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '');
|
const id = name.toLowerCase().replace(/\s+/g, '_').replace(/[^\p{L}\p{N}_]/gu, '');
|
||||||
result.stats.push({
|
result.stats.push({
|
||||||
id: id,
|
id: id,
|
||||||
name: name,
|
name: name,
|
||||||
|
|||||||
+11
-6
@@ -11,13 +11,17 @@
|
|||||||
* @returns {object|null} Repaired JSON object or null if repair fails
|
* @returns {object|null} Repaired JSON object or null if repair fails
|
||||||
*/
|
*/
|
||||||
export function repairJSON(jsonString) {
|
export function repairJSON(jsonString) {
|
||||||
if (!jsonString || typeof jsonString !== 'string') {
|
if (typeof jsonString !== 'string') {
|
||||||
console.warn('[RPG JSON Repair] Invalid input:', typeof jsonString);
|
console.warn('[RPG JSON Repair] Invalid input type:', typeof jsonString);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cleaned = jsonString.trim();
|
let cleaned = jsonString.trim();
|
||||||
|
|
||||||
|
if (!cleaned) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove markdown code fences
|
// Remove markdown code fences
|
||||||
cleaned = cleaned.replace(/```json\s*/gi, '');
|
cleaned = cleaned.replace(/```json\s*/gi, '');
|
||||||
cleaned = cleaned.replace(/```\s*/g, '');
|
cleaned = cleaned.replace(/```\s*/g, '');
|
||||||
@@ -147,7 +151,8 @@ export function extractJSONFromText(text) {
|
|||||||
// Try to extract from ```json code fence
|
// Try to extract from ```json code fence
|
||||||
const fenceMatch = text.match(/```json\s*([\s\S]*?)```/i);
|
const fenceMatch = text.match(/```json\s*([\s\S]*?)```/i);
|
||||||
if (fenceMatch && fenceMatch[1]) {
|
if (fenceMatch && fenceMatch[1]) {
|
||||||
return fenceMatch[1].trim();
|
const trimmed = fenceMatch[1].trim();
|
||||||
|
if (trimmed) return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to extract from ``` code fence (without json label)
|
// Try to extract from ``` code fence (without json label)
|
||||||
@@ -155,20 +160,20 @@ export function extractJSONFromText(text) {
|
|||||||
if (genericFenceMatch && genericFenceMatch[1]) {
|
if (genericFenceMatch && genericFenceMatch[1]) {
|
||||||
const content = genericFenceMatch[1].trim();
|
const content = genericFenceMatch[1].trim();
|
||||||
// Check if it looks like JSON (starts with { or [)
|
// Check if it looks like JSON (starts with { or [)
|
||||||
if (content.startsWith('{') || content.startsWith('[')) {
|
if (content && (content.startsWith('{') || content.startsWith('['))) {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find standalone JSON object
|
// Try to find standalone JSON object
|
||||||
const objectMatch = text.match(/\{[\s\S]*\}/);
|
const objectMatch = text.match(/\{[\s\S]*\}/);
|
||||||
if (objectMatch) {
|
if (objectMatch && objectMatch[0].trim()) {
|
||||||
return objectMatch[0];
|
return objectMatch[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find standalone JSON array
|
// Try to find standalone JSON array
|
||||||
const arrayMatch = text.match(/\[[\s\S]*\]/);
|
const arrayMatch = text.match(/\[[\s\S]*\]/);
|
||||||
if (arrayMatch) {
|
if (arrayMatch && arrayMatch[0].trim()) {
|
||||||
return arrayMatch[0];
|
return arrayMatch[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
import { this_chid, characters } from '../../../../../../script.js';
|
||||||
|
import { selected_group, getGroupMembers } from '../../../../../group-chats.js';
|
||||||
|
import {
|
||||||
|
extensionSettings,
|
||||||
|
lastGeneratedData,
|
||||||
|
committedTrackerData,
|
||||||
|
FALLBACK_AVATAR_DATA_URI
|
||||||
|
} from '../core/state.js';
|
||||||
|
import { getSafeThumbnailUrl } from './avatars.js';
|
||||||
|
|
||||||
|
export function stripBrackets(value) {
|
||||||
|
if (typeof value !== 'string') return value;
|
||||||
|
return value.replace(/^\[|\]$/g, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractFieldValue(fieldValue) {
|
||||||
|
if (fieldValue && typeof fieldValue === 'object' && 'value' in fieldValue) {
|
||||||
|
return fieldValue.value || '';
|
||||||
|
}
|
||||||
|
return fieldValue || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toSnakeCase(name) {
|
||||||
|
return name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^\p{L}\p{N}]+/gu, '_')
|
||||||
|
.replace(/^_+|_+$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function namesMatch(cardName, aiName) {
|
||||||
|
if (!cardName || !aiName) return false;
|
||||||
|
|
||||||
|
if (cardName.toLowerCase() === aiName.toLowerCase()) return true;
|
||||||
|
|
||||||
|
const stripParens = (s) => s.replace(/\s*\([^)]*\)/g, '').trim();
|
||||||
|
const cardCore = stripParens(cardName).toLowerCase();
|
||||||
|
const aiCore = stripParens(aiName).toLowerCase();
|
||||||
|
if (cardCore === aiCore) return true;
|
||||||
|
|
||||||
|
const escapedCardCore = cardCore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
const wordBoundary = new RegExp(`\\b${escapedCardCore}\\b`);
|
||||||
|
return wordBoundary.test(aiCore);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPresentCharactersEnabled() {
|
||||||
|
return !!(
|
||||||
|
extensionSettings.showCharacterThoughts
|
||||||
|
|| extensionSettings.showAlternatePresentCharactersPanel
|
||||||
|
|| extensionSettings.showThoughtsInChat
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPresentCharactersTrackerData({ useCommittedFallback = true } = {}) {
|
||||||
|
return lastGeneratedData.characterThoughts || (useCommittedFallback ? committedTrackerData.characterThoughts : null) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePresentCharacters(characterThoughtsData, { enabledFields = [], enabledCharStats = [] } = {}) {
|
||||||
|
if (!characterThoughtsData) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let presentCharacters = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = typeof characterThoughtsData === 'string'
|
||||||
|
? JSON.parse(characterThoughtsData)
|
||||||
|
: characterThoughtsData;
|
||||||
|
|
||||||
|
const charactersArray = Array.isArray(parsed) ? parsed : (parsed.characters || []);
|
||||||
|
|
||||||
|
if (charactersArray.length > 0) {
|
||||||
|
presentCharacters = charactersArray.map(char => {
|
||||||
|
const character = {
|
||||||
|
name: char.name,
|
||||||
|
emoji: char.emoji || '👤'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (char.details) {
|
||||||
|
for (const field of enabledFields) {
|
||||||
|
if (char.details[field.name] !== undefined) {
|
||||||
|
character[field.name] = stripBrackets(char.details[field.name]);
|
||||||
|
} else {
|
||||||
|
const fieldKey = toSnakeCase(field.name);
|
||||||
|
if (char.details[fieldKey] !== undefined) {
|
||||||
|
character[field.name] = stripBrackets(char.details[fieldKey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of enabledFields) {
|
||||||
|
if (character[field.name] === undefined) {
|
||||||
|
const fieldKey = toSnakeCase(field.name);
|
||||||
|
if (char[fieldKey] !== undefined) {
|
||||||
|
character[field.name] = stripBrackets(char[fieldKey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.Relationship) {
|
||||||
|
character.Relationship = stripBrackets(char.Relationship);
|
||||||
|
} else if (char.relationship) {
|
||||||
|
character.Relationship = stripBrackets(char.relationship.status || char.relationship);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.thoughts) {
|
||||||
|
character.ThoughtsContent = stripBrackets(char.thoughts.content || char.thoughts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.stats && enabledCharStats.length > 0) {
|
||||||
|
if (Array.isArray(char.stats)) {
|
||||||
|
for (const statObj of char.stats) {
|
||||||
|
if (statObj.name && statObj.value !== undefined) {
|
||||||
|
const matchingStat = enabledCharStats.find(s => s.name === statObj.name);
|
||||||
|
if (matchingStat) {
|
||||||
|
character[statObj.name] = statObj.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const stat of enabledCharStats) {
|
||||||
|
if (char.stats[stat.name] !== undefined) {
|
||||||
|
character[stat.name] = char.stats[stat.name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return character;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to the legacy text format below.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presentCharacters.length > 0 || typeof characterThoughtsData !== 'string') {
|
||||||
|
return presentCharacters;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = characterThoughtsData.split('\n');
|
||||||
|
let currentCharacter = null;
|
||||||
|
const thoughtsLabel = extensionSettings.trackerConfig?.presentCharacters?.thoughts?.name || 'Thoughts';
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()
|
||||||
|
|| line.includes('Present Characters')
|
||||||
|
|| line.includes('---')
|
||||||
|
|| line.trim().startsWith('```')
|
||||||
|
|| line.trim() === '- …'
|
||||||
|
|| line.includes('(Repeat the format')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.trim().startsWith('- ')) {
|
||||||
|
const name = line.trim().substring(2).trim();
|
||||||
|
|
||||||
|
if (name && name.toLowerCase() !== 'unavailable') {
|
||||||
|
currentCharacter = { name };
|
||||||
|
presentCharacters.push(currentCharacter);
|
||||||
|
} else {
|
||||||
|
currentCharacter = null;
|
||||||
|
}
|
||||||
|
} else if (line.trim().startsWith('Details:') && currentCharacter) {
|
||||||
|
const detailsContent = line.substring(line.indexOf(':') + 1).trim();
|
||||||
|
const parts = detailsContent.split('|').map(p => p.trim());
|
||||||
|
|
||||||
|
if (parts.length > 0) {
|
||||||
|
currentCharacter.emoji = parts[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < enabledFields.length && i + 1 < parts.length; i++) {
|
||||||
|
currentCharacter[enabledFields[i].name] = parts[i + 1];
|
||||||
|
}
|
||||||
|
} else if (line.trim().startsWith('Relationship:') && currentCharacter) {
|
||||||
|
currentCharacter.Relationship = line.substring(line.indexOf(':') + 1).trim();
|
||||||
|
} else if (line.trim().startsWith('Stats:') && currentCharacter && enabledCharStats.length > 0) {
|
||||||
|
const statsContent = line.substring(line.indexOf(':') + 1).trim();
|
||||||
|
const statParts = statsContent.split('|').map(p => p.trim());
|
||||||
|
|
||||||
|
for (const statPart of statParts) {
|
||||||
|
const statMatch = statPart.match(/^(.+?):\s*(\d+)%$/);
|
||||||
|
if (statMatch) {
|
||||||
|
currentCharacter[statMatch[1].trim()] = parseInt(statMatch[2], 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (line.trim().startsWith(thoughtsLabel + ':') && currentCharacter) {
|
||||||
|
currentCharacter.ThoughtsContent = line.substring(line.indexOf(':') + 1).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return presentCharacters;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePresentCharacterPortrait(name) {
|
||||||
|
let characterPortrait = FALLBACK_AVATAR_DATA_URI;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return characterPortrait;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensionSettings.npcAvatars && extensionSettings.npcAvatars[name]) {
|
||||||
|
return extensionSettings.npcAvatars[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected_group) {
|
||||||
|
try {
|
||||||
|
const groupMembers = getGroupMembers(selected_group);
|
||||||
|
const matchingMember = groupMembers?.find(member =>
|
||||||
|
member && member.name && namesMatch(member.name, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingMember?.avatar && matchingMember.avatar !== 'none') {
|
||||||
|
const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingMember.avatar);
|
||||||
|
if (thumbnailUrl) {
|
||||||
|
return thumbnailUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore avatar lookup issues and continue through fallback chain.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (characters?.length > 0) {
|
||||||
|
const matchingCharacter = characters.find(character =>
|
||||||
|
character && character.name && namesMatch(character.name, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingCharacter?.avatar && matchingCharacter.avatar !== 'none') {
|
||||||
|
const thumbnailUrl = getSafeThumbnailUrl('avatar', matchingCharacter.avatar);
|
||||||
|
if (thumbnailUrl) {
|
||||||
|
return thumbnailUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this_chid !== undefined && characters?.[this_chid]?.name && namesMatch(characters[this_chid].name, name)) {
|
||||||
|
const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar);
|
||||||
|
if (thumbnailUrl) {
|
||||||
|
return thumbnailUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return characterPortrait;
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Response Extractor Utility
|
||||||
|
*
|
||||||
|
* Handles extraction of text content from various API response formats.
|
||||||
|
* Fixes the "No message generated" error caused by Claude models with
|
||||||
|
* extended thinking, where the API response `content` field is an array
|
||||||
|
* of content blocks instead of a single string.
|
||||||
|
*
|
||||||
|
* Also provides a safe wrapper around SillyTavern's `generateRaw` that
|
||||||
|
* intercepts the raw fetch response as a fallback.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { generateRaw } from '../../../../../../../script.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts text from any API response shape (Anthropic content-block arrays,
|
||||||
|
* OpenAI choices, plain strings, etc.).
|
||||||
|
*
|
||||||
|
* @param {*} response - The raw API response (string, array, or object)
|
||||||
|
* @returns {string} The extracted text content
|
||||||
|
*/
|
||||||
|
export function extractTextFromResponse(response) {
|
||||||
|
if (!response) return '';
|
||||||
|
if (typeof response === 'string') return response;
|
||||||
|
|
||||||
|
// Response itself is an array of content blocks (Anthropic extended thinking)
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
const texts = response
|
||||||
|
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
|
||||||
|
.map(b => b.text);
|
||||||
|
if (texts.length > 0) return texts.join('\n');
|
||||||
|
|
||||||
|
const strings = response.filter(item => typeof item === 'string');
|
||||||
|
if (strings.length > 0) return strings.join('\n');
|
||||||
|
|
||||||
|
return JSON.stringify(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// response.content (string or Anthropic content array)
|
||||||
|
if (response.content !== undefined && response.content !== null) {
|
||||||
|
if (typeof response.content === 'string') return response.content;
|
||||||
|
if (Array.isArray(response.content)) {
|
||||||
|
const texts = response.content
|
||||||
|
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
|
||||||
|
.map(b => b.text);
|
||||||
|
if (texts.length > 0) return texts.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAI choices format
|
||||||
|
if (response.choices?.[0]?.message?.content) {
|
||||||
|
const c = response.choices[0].message.content;
|
||||||
|
if (typeof c === 'string') return c;
|
||||||
|
if (Array.isArray(c)) {
|
||||||
|
const texts = c
|
||||||
|
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
|
||||||
|
.map(b => b.text);
|
||||||
|
if (texts.length > 0) return texts.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other common fields
|
||||||
|
if (typeof response.text === 'string') return response.text;
|
||||||
|
if (typeof response.message === 'string') return response.message;
|
||||||
|
if (response.message?.content && typeof response.message.content === 'string') {
|
||||||
|
return response.message.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe wrapper around SillyTavern's `generateRaw`.
|
||||||
|
*
|
||||||
|
* Temporarily intercepts `window.fetch` to capture the raw API response.
|
||||||
|
* If `generateRaw` throws "No message generated" (e.g. because the first
|
||||||
|
* content block from Claude extended thinking is empty), we extract the
|
||||||
|
* real text from the captured raw data ourselves.
|
||||||
|
*
|
||||||
|
* @param {object} options - Options passed directly to `generateRaw`
|
||||||
|
* @param {Array<{role: string, content: string}>} options.prompt - Message array
|
||||||
|
* @param {boolean} [options.quietToLoud] - Whether to use quiet-to-loud mode
|
||||||
|
* @returns {Promise<string>} The generated text
|
||||||
|
*/
|
||||||
|
export async function safeGenerateRaw(options) {
|
||||||
|
let capturedRawData = null;
|
||||||
|
const originalFetch = window.fetch;
|
||||||
|
|
||||||
|
window.fetch = async function (...args) {
|
||||||
|
const response = await originalFetch.apply(this, args);
|
||||||
|
try {
|
||||||
|
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
|
||||||
|
if (url.includes('/api/backends/chat-completions/generate') ||
|
||||||
|
(url.includes('/api/backends/') && url.includes('/generate'))) {
|
||||||
|
const clone = response.clone();
|
||||||
|
capturedRawData = await clone.json();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore clone/parse errors */
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await generateRaw(options);
|
||||||
|
return result;
|
||||||
|
} catch (genErr) {
|
||||||
|
if (genErr.message?.includes('No message generated') && capturedRawData) {
|
||||||
|
console.warn(
|
||||||
|
'[RPG Companion] generateRaw failed (likely extended thinking). Extracting from raw API data.',
|
||||||
|
);
|
||||||
|
const extracted = extractTextFromResponse(capturedRawData);
|
||||||
|
if (!extracted || !extracted.trim()) {
|
||||||
|
throw new Error('Could not extract text from API response');
|
||||||
|
}
|
||||||
|
return extracted;
|
||||||
|
}
|
||||||
|
throw genErr; // Re-throw non-related errors
|
||||||
|
} finally {
|
||||||
|
window.fetch = originalFetch; // ALWAYS restore original fetch
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,626 @@
|
|||||||
|
import { Fuse } from '../../../../../../lib.js';
|
||||||
|
import {
|
||||||
|
characters,
|
||||||
|
eventSource,
|
||||||
|
event_types,
|
||||||
|
generateQuietPrompt,
|
||||||
|
generateRaw,
|
||||||
|
getRequestHeaders,
|
||||||
|
online_status,
|
||||||
|
substituteParams,
|
||||||
|
substituteParamsExtended,
|
||||||
|
this_chid
|
||||||
|
} from '../../../../../../script.js';
|
||||||
|
import {
|
||||||
|
doExtrasFetch,
|
||||||
|
extension_settings as stExtensionSettings,
|
||||||
|
getApiUrl,
|
||||||
|
modules
|
||||||
|
} from '../../../../../extensions.js';
|
||||||
|
import { selected_group, getGroupMembers } from '../../../../../group-chats.js';
|
||||||
|
import { removeReasoningFromString } from '../../../../../reasoning.js';
|
||||||
|
import { isJsonSchemaSupported } from '../../../../../textgen-settings.js';
|
||||||
|
import { trimToEndSentence, trimToStartSentence, waitUntilCondition } from '../../../../../utils.js';
|
||||||
|
import { generateWebLlmChatPrompt, isWebLlmSupported } from '../../../../../extensions/shared.js';
|
||||||
|
import { namesMatch } from './presentCharacters.js';
|
||||||
|
import { normalizeImageSrc } from './imageUrls.js';
|
||||||
|
|
||||||
|
const EXPRESSIONS_EXTENSION_NAME = 'expressions';
|
||||||
|
const DEFAULT_FALLBACK_EXPRESSION = 'joy';
|
||||||
|
const DEFAULT_LLM_PROMPT = 'Ignore previous instructions. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';
|
||||||
|
const DEFAULT_EXPRESSIONS = [
|
||||||
|
'admiration',
|
||||||
|
'amusement',
|
||||||
|
'anger',
|
||||||
|
'annoyance',
|
||||||
|
'approval',
|
||||||
|
'caring',
|
||||||
|
'confusion',
|
||||||
|
'curiosity',
|
||||||
|
'desire',
|
||||||
|
'disappointment',
|
||||||
|
'disapproval',
|
||||||
|
'disgust',
|
||||||
|
'embarrassment',
|
||||||
|
'excitement',
|
||||||
|
'fear',
|
||||||
|
'gratitude',
|
||||||
|
'grief',
|
||||||
|
'joy',
|
||||||
|
'love',
|
||||||
|
'nervousness',
|
||||||
|
'optimism',
|
||||||
|
'pride',
|
||||||
|
'realization',
|
||||||
|
'relief',
|
||||||
|
'remorse',
|
||||||
|
'sadness',
|
||||||
|
'surprise',
|
||||||
|
'neutral'
|
||||||
|
];
|
||||||
|
|
||||||
|
export const EXPRESSION_API = {
|
||||||
|
local: 0,
|
||||||
|
extras: 1,
|
||||||
|
llm: 2,
|
||||||
|
webllm: 3,
|
||||||
|
none: 99
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROMPT_TYPE = {
|
||||||
|
raw: 'raw',
|
||||||
|
full: 'full'
|
||||||
|
};
|
||||||
|
|
||||||
|
let expressionsListCache = null;
|
||||||
|
const spriteCache = new Map();
|
||||||
|
|
||||||
|
function getNormalizedExpressionsSettings() {
|
||||||
|
const settings = stExtensionSettings.expressions || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
api: Number.isInteger(settings.api) ? settings.api : EXPRESSION_API.none,
|
||||||
|
custom: Array.isArray(settings.custom) ? settings.custom.slice() : [],
|
||||||
|
showDefault: settings.showDefault === true,
|
||||||
|
translate: settings.translate === true,
|
||||||
|
fallbackExpression: typeof settings.fallback_expression === 'string' && settings.fallback_expression.trim()
|
||||||
|
? settings.fallback_expression.trim().toLowerCase()
|
||||||
|
: '',
|
||||||
|
llmPrompt: typeof settings.llmPrompt === 'string' && settings.llmPrompt.trim()
|
||||||
|
? settings.llmPrompt
|
||||||
|
: DEFAULT_LLM_PROMPT,
|
||||||
|
allowMultiple: settings.allowMultiple !== false,
|
||||||
|
rerollIfSame: settings.rerollIfSame === true,
|
||||||
|
filterAvailable: settings.filterAvailable === true,
|
||||||
|
promptType: settings.promptType === PROMPT_TYPE.full ? PROMPT_TYPE.full : PROMPT_TYPE.raw,
|
||||||
|
expressionOverrides: Array.isArray(stExtensionSettings.expressionOverrides)
|
||||||
|
? stExtensionSettings.expressionOverrides.slice()
|
||||||
|
: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isExpressionsExtensionEnabled() {
|
||||||
|
return !stExtensionSettings.disabledExtensions?.includes(EXPRESSIONS_EXTENSION_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExpressionsSettingsSignature() {
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
return 'disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
return JSON.stringify({
|
||||||
|
api: settings.api,
|
||||||
|
custom: settings.custom,
|
||||||
|
showDefault: settings.showDefault,
|
||||||
|
translate: settings.translate,
|
||||||
|
fallbackExpression: settings.fallbackExpression,
|
||||||
|
llmPrompt: settings.llmPrompt,
|
||||||
|
allowMultiple: settings.allowMultiple,
|
||||||
|
rerollIfSame: settings.rerollIfSame,
|
||||||
|
filterAvailable: settings.filterAvailable,
|
||||||
|
promptType: settings.promptType,
|
||||||
|
expressionOverrides: settings.expressionOverrides
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExpressionClassificationSettingsSignature() {
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
return 'disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
return JSON.stringify({
|
||||||
|
api: settings.api,
|
||||||
|
custom: settings.custom,
|
||||||
|
translate: settings.translate,
|
||||||
|
fallbackExpression: settings.fallbackExpression,
|
||||||
|
llmPrompt: settings.llmPrompt,
|
||||||
|
filterAvailable: settings.filterAvailable,
|
||||||
|
promptType: settings.promptType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExpressionPortraitSettingsSignature() {
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
return 'disabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
return JSON.stringify({
|
||||||
|
custom: settings.custom,
|
||||||
|
showDefault: settings.showDefault,
|
||||||
|
fallbackExpression: settings.fallbackExpression,
|
||||||
|
allowMultiple: settings.allowMultiple,
|
||||||
|
rerollIfSame: settings.rerollIfSame
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearExpressionsCompatibilityCache() {
|
||||||
|
expressionsListCache = null;
|
||||||
|
spriteCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueValues(values) {
|
||||||
|
return values.filter((value, index) => values.indexOf(value) === index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeExpressionLabel(label) {
|
||||||
|
return String(label || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripExtension(fileName) {
|
||||||
|
return String(fileName || '').replace(/\.[^/.]+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFolderOverride(folderName, expressionOverrides) {
|
||||||
|
const override = expressionOverrides.find(entry => entry?.name === folderName);
|
||||||
|
return override?.path ? String(override.path) : folderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAvatarFolderName(avatar) {
|
||||||
|
if (!avatar || avatar === 'none') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(avatar).replace(/\.[^/.]+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSpriteFolderNameForCharacter(characterName) {
|
||||||
|
if (!characterName) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
const groupId = selected_group;
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
try {
|
||||||
|
const groupMembers = getGroupMembers(groupId) || [];
|
||||||
|
const matchingMember = groupMembers.find(member =>
|
||||||
|
member?.name && namesMatch(member.name, characterName));
|
||||||
|
|
||||||
|
const memberFolder = getAvatarFolderName(matchingMember?.avatar);
|
||||||
|
if (memberFolder) {
|
||||||
|
return resolveFolderOverride(memberFolder, settings.expressionOverrides);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore group lookup issues and continue through the fallback chain.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(characters) && characters.length > 0) {
|
||||||
|
const matchingCharacter = characters.find(character =>
|
||||||
|
character?.name && namesMatch(character.name, characterName));
|
||||||
|
|
||||||
|
const characterFolder = getAvatarFolderName(matchingCharacter?.avatar);
|
||||||
|
if (characterFolder) {
|
||||||
|
return resolveFolderOverride(characterFolder, settings.expressionOverrides);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this_chid !== undefined && characters?.[this_chid]?.name && namesMatch(characters[this_chid].name, characterName)) {
|
||||||
|
const currentCharacterFolder = getAvatarFolderName(characters[this_chid].avatar);
|
||||||
|
if (currentCharacterFolder) {
|
||||||
|
return resolveFolderOverride(currentCharacterFolder, settings.expressionOverrides);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sampleClassifyText(text, expressionsApi) {
|
||||||
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = substituteParams(text).replace(/[*"]/g, '');
|
||||||
|
|
||||||
|
if (expressionsApi === EXPRESSION_API.llm) {
|
||||||
|
return result.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const SAMPLE_THRESHOLD = 500;
|
||||||
|
const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2;
|
||||||
|
|
||||||
|
if (text.length < SAMPLE_THRESHOLD) {
|
||||||
|
result = trimToEndSentence(result);
|
||||||
|
} else {
|
||||||
|
result = `${trimToEndSentence(result.slice(0, HALF_SAMPLE_THRESHOLD))} ${trimToStartSentence(result.slice(-HALF_SAMPLE_THRESHOLD))}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJsonSchema(labels) {
|
||||||
|
return {
|
||||||
|
$schema: 'http://json-schema.org/draft-04/schema#',
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
emotion: {
|
||||||
|
type: 'string',
|
||||||
|
enum: labels
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['emotion'],
|
||||||
|
additionalProperties: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFullContextThoughtPrompt(prompt, text) {
|
||||||
|
return [
|
||||||
|
prompt,
|
||||||
|
'',
|
||||||
|
'Classify the emotion of the following text instead of the last chat message.',
|
||||||
|
'Output exactly one label from the allowed list.',
|
||||||
|
'',
|
||||||
|
`Text: ${text}`
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLlmResponse(emotionResponse, labels) {
|
||||||
|
try {
|
||||||
|
const parsedEmotion = JSON.parse(emotionResponse);
|
||||||
|
const response = parsedEmotion?.emotion?.trim()?.toLowerCase();
|
||||||
|
|
||||||
|
if (response && labels.includes(response)) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to the fuzzy parse below.
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedResponse = removeReasoningFromString(String(emotionResponse || ''));
|
||||||
|
const lowerCaseResponse = cleanedResponse.toLowerCase();
|
||||||
|
|
||||||
|
for (const label of labels) {
|
||||||
|
if (lowerCaseResponse.includes(label.toLowerCase())) {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fuse = new Fuse(labels, { includeScore: true });
|
||||||
|
const match = fuse.search(cleanedResponse)[0];
|
||||||
|
if (match?.item) {
|
||||||
|
return match.item;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Could not parse expression label from response');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveExpressionsList() {
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (settings.api === EXPRESSION_API.extras && modules.includes('classify')) {
|
||||||
|
const url = new URL(getApiUrl());
|
||||||
|
url.pathname = '/api/classify/labels';
|
||||||
|
|
||||||
|
const response = await doExtrasFetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Bypass-Tunnel-Reminder': 'bypass' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
return Array.isArray(data?.labels)
|
||||||
|
? data.labels.map(normalizeExpressionLabel).filter(Boolean)
|
||||||
|
: DEFAULT_EXPRESSIONS.slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.api === EXPRESSION_API.local) {
|
||||||
|
const response = await fetch('/api/extra/classify/labels', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders({ omitContentType: true })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
return Array.isArray(data?.labels)
|
||||||
|
? data.labels.map(normalizeExpressionLabel).filter(Boolean)
|
||||||
|
: DEFAULT_EXPRESSIONS.slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to the built-in labels below.
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_EXPRESSIONS.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAvailableExpressionLabelsForCharacter(characterName) {
|
||||||
|
const spriteFolderName = resolveSpriteFolderNameForCharacter(characterName);
|
||||||
|
if (!spriteFolderName) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const expressions = await getSpritesList(spriteFolderName);
|
||||||
|
return expressions
|
||||||
|
.filter(expression => Array.isArray(expression?.files) && expression.files.length > 0)
|
||||||
|
.map(expression => String(expression.label || '').trim().toLowerCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getExpressionsList({ characterName = '', filterAvailable = false } = {}) {
|
||||||
|
if (!Array.isArray(expressionsListCache)) {
|
||||||
|
expressionsListCache = await resolveExpressionsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
const expressions = uniqueValues([...expressionsListCache, ...settings.custom.map(value => String(value).trim().toLowerCase())])
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (!filterAvailable || ![EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(settings.api)) {
|
||||||
|
return expressions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableExpressions = await getAvailableExpressionLabelsForCharacter(characterName);
|
||||||
|
if (!availableExpressions.length) {
|
||||||
|
return expressions;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expressions.filter(expression => availableExpressions.includes(expression));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSpritesList(spriteFolderName) {
|
||||||
|
if (!spriteFolderName) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spriteCache.has(spriteFolderName)) {
|
||||||
|
return spriteCache.get(spriteFolderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/sprites/get?name=${encodeURIComponent(spriteFolderName)}`);
|
||||||
|
const sprites = response.ok ? await response.json() : [];
|
||||||
|
const grouped = [];
|
||||||
|
|
||||||
|
for (const sprite of Array.isArray(sprites) ? sprites : []) {
|
||||||
|
const fileName = String(sprite?.path || '').split('/').pop()?.split('?')[0] || '';
|
||||||
|
const imageData = {
|
||||||
|
expression: normalizeExpressionLabel(sprite?.label),
|
||||||
|
fileName,
|
||||||
|
title: stripExtension(fileName),
|
||||||
|
imageSrc: String(sprite?.path || ''),
|
||||||
|
type: 'success',
|
||||||
|
isCustom: getNormalizedExpressionsSettings().custom.includes(normalizeExpressionLabel(sprite?.label))
|
||||||
|
};
|
||||||
|
|
||||||
|
let existing = grouped.find(entry => entry.label === imageData.expression);
|
||||||
|
if (!existing) {
|
||||||
|
existing = { label: imageData.expression, files: [] };
|
||||||
|
grouped.push(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.files.push(imageData);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const expression of grouped) {
|
||||||
|
expression.files.sort((left, right) => {
|
||||||
|
if (left.title === expression.label) return -1;
|
||||||
|
if (right.title === expression.label) return 1;
|
||||||
|
return left.title.localeCompare(right.title);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
spriteCache.set(spriteFolderName, grouped);
|
||||||
|
return grouped;
|
||||||
|
} catch {
|
||||||
|
spriteCache.set(spriteFolderName, []);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseSpriteForExpression(expressions, expression, { previousSrc = null } = {}) {
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
let sprite = expressions.find(entry => entry.label === expression);
|
||||||
|
|
||||||
|
if (!(sprite?.files?.length > 0) && settings.fallbackExpression) {
|
||||||
|
sprite = expressions.find(entry => entry.label === settings.fallbackExpression);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(sprite?.files?.length > 0)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let candidates = sprite.files;
|
||||||
|
if (settings.allowMultiple && sprite.files.length > 1) {
|
||||||
|
if (settings.rerollIfSame) {
|
||||||
|
const filtered = sprite.files.filter(file => !previousSrc || file.imageSrc !== previousSrc);
|
||||||
|
if (filtered.length > 0) {
|
||||||
|
candidates = filtered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates[Math.floor(Math.random() * candidates.length)] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultExpressionImage(expression, customExpressions) {
|
||||||
|
let normalizedExpression = String(expression || '').trim().toLowerCase();
|
||||||
|
|
||||||
|
if (!normalizedExpression) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customExpressions.includes(normalizedExpression)) {
|
||||||
|
normalizedExpression = DEFAULT_FALLBACK_EXPRESSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/img/default-expressions/${normalizedExpression}.png`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function classifyExpressionText(text, { characterName = '' } = {}) {
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
if (!text) {
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.api === EXPRESSION_API.none) {
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let processedText = text;
|
||||||
|
if (settings.translate && typeof globalThis.translate === 'function') {
|
||||||
|
processedText = await globalThis.translate(processedText, 'en');
|
||||||
|
}
|
||||||
|
|
||||||
|
processedText = sampleClassifyText(processedText, settings.api);
|
||||||
|
if (!processedText) {
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = await getExpressionsList({
|
||||||
|
characterName,
|
||||||
|
filterAvailable: settings.filterAvailable === true
|
||||||
|
});
|
||||||
|
const fallbackLabels = labels.length > 0 ? labels : await getExpressionsList();
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (settings.api) {
|
||||||
|
case EXPRESSION_API.local: {
|
||||||
|
const response = await fetch('/api/extra/classify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
body: JSON.stringify({ text: processedText })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
return String(data?.classification?.[0]?.label || settings.fallbackExpression || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EXPRESSION_API.extras: {
|
||||||
|
if (!modules.includes('classify')) {
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(getApiUrl());
|
||||||
|
url.pathname = '/api/classify';
|
||||||
|
|
||||||
|
const response = await doExtrasFetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Bypass-Tunnel-Reminder': 'bypass'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ text: processedText })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
return String(data?.classification?.[0]?.label || settings.fallbackExpression || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EXPRESSION_API.llm: {
|
||||||
|
await waitUntilCondition(() => online_status !== 'no_connection', 3000, 250);
|
||||||
|
|
||||||
|
const labelsString = fallbackLabels.map(label => `"${label}"`).join(', ');
|
||||||
|
const basePrompt = substituteParamsExtended(settings.llmPrompt, { labels: labelsString });
|
||||||
|
const prompt = settings.promptType === PROMPT_TYPE.full
|
||||||
|
? buildFullContextThoughtPrompt(basePrompt, processedText)
|
||||||
|
: basePrompt;
|
||||||
|
const onReady = (args) => {
|
||||||
|
if (isJsonSchemaSupported()) {
|
||||||
|
Object.assign(args, {
|
||||||
|
top_k: 1,
|
||||||
|
stop: [],
|
||||||
|
stopping_strings: [],
|
||||||
|
custom_token_bans: [],
|
||||||
|
json_schema: getJsonSchema(fallbackLabels)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onReady);
|
||||||
|
|
||||||
|
const responseText = settings.promptType === PROMPT_TYPE.full
|
||||||
|
? await generateQuietPrompt({ quietPrompt: prompt })
|
||||||
|
: await generateRaw({ prompt: processedText, systemPrompt: prompt });
|
||||||
|
|
||||||
|
return parseLlmResponse(responseText, fallbackLabels);
|
||||||
|
}
|
||||||
|
case EXPRESSION_API.webllm: {
|
||||||
|
if (!isWebLlmSupported()) {
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelsString = fallbackLabels.map(label => `"${label}"`).join(', ');
|
||||||
|
const prompt = substituteParamsExtended(settings.llmPrompt, { labels: labelsString });
|
||||||
|
const responseText = await generateWebLlmChatPrompt([
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: `${processedText}\n\n${prompt}`
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
return parseLlmResponse(responseText, fallbackLabels);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings.fallbackExpression || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveExpressionPortraitForCharacter(characterName, expression, { previousSrc = null } = {}) {
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = getNormalizedExpressionsSettings();
|
||||||
|
const normalizedExpression = String(expression || '').trim().toLowerCase();
|
||||||
|
const spriteFolderName = resolveSpriteFolderNameForCharacter(characterName);
|
||||||
|
|
||||||
|
if (spriteFolderName) {
|
||||||
|
const expressions = await getSpritesList(spriteFolderName);
|
||||||
|
const spriteFile = chooseSpriteForExpression(expressions, normalizedExpression, { previousSrc });
|
||||||
|
const spriteSrc = normalizeImageSrc(spriteFile?.imageSrc || '');
|
||||||
|
if (spriteSrc) {
|
||||||
|
return spriteSrc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.showDefault) {
|
||||||
|
const defaultExpression = normalizedExpression || settings.fallbackExpression;
|
||||||
|
const defaultImage = normalizeImageSrc(getDefaultExpressionImage(defaultExpression, settings.custom));
|
||||||
|
if (defaultImage) {
|
||||||
|
return defaultImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
thoughtBasedExpressionPortraits,
|
||||||
|
getThoughtBasedExpressionPortrait
|
||||||
|
} from '../core/state.js';
|
||||||
|
import {
|
||||||
|
isSafeImageSrc,
|
||||||
|
normalizeImageSrc,
|
||||||
|
resolveImageUrl
|
||||||
|
} from './imageUrls.js';
|
||||||
|
import { isExpressionsExtensionEnabled } from './sillyTavernExpressions.js';
|
||||||
|
|
||||||
|
function normalizeName(name) {
|
||||||
|
return String(name || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function namesMatch(a, b) {
|
||||||
|
const left = normalizeName(a);
|
||||||
|
const right = normalizeName(b);
|
||||||
|
if (!left || !right) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return left === right || left.startsWith(right + ' ') || right.startsWith(left + ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDocumentLikeUrl(src) {
|
||||||
|
const candidate = resolveImageUrl(src);
|
||||||
|
if (!candidate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = new URL(window.location.href);
|
||||||
|
return candidate.origin === current.origin
|
||||||
|
&& candidate.pathname === current.pathname
|
||||||
|
&& candidate.search === current.search;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUsableThoughtBasedExpressionSrc(src) {
|
||||||
|
const normalized = normalizeImageSrc(src);
|
||||||
|
if (!normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDocumentLikeUrl(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSafeImageSrc(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThoughtBasedExpressionPortraitForCharacter(characterName) {
|
||||||
|
if (!isExpressionsExtensionEnabled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = normalizeName(characterName);
|
||||||
|
if (!target) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exact = getThoughtBasedExpressionPortrait(target);
|
||||||
|
if (isUsableThoughtBasedExpressionSrc(exact)) {
|
||||||
|
return exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [storedName, src] of Object.entries(thoughtBasedExpressionPortraits)) {
|
||||||
|
if (namesMatch(storedName, target) && isUsableThoughtBasedExpressionSrc(src)) {
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
const toSnake = (str) => str
|
||||||
|
// replace any sequence of non-alphanumeric characters with a single underscore
|
||||||
|
.replace(/[^0-9A-Za-z]+/g, '_')
|
||||||
|
// insert underscore between a lower-case letter/digit and an upper-case letter (but not between consecutive uppers)
|
||||||
|
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
||||||
|
// collapse multiple underscores
|
||||||
|
.replace(/_+/g, '_')
|
||||||
|
// trim leading/trailing underscores
|
||||||
|
.replace(/^_+|_+$/g, '')
|
||||||
|
// finally, lowercase the result
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
export const safeToSnake = (str) => {
|
||||||
|
const res = toSnake(str);
|
||||||
|
return (res.length >= 2) ? res : str; // considering element with one symbol is too short to be safe
|
||||||
|
};
|
||||||
+483
-75
@@ -1,9 +1,49 @@
|
|||||||
<div id="rpg-companion-panel" class="rpg-panel">
|
<div id="rpg-companion-panel" class="rpg-panel">
|
||||||
<!-- Collapse/Expand Toggle Button -->
|
<!-- Collapse/Expand Toggle Button -->
|
||||||
<button class="rpg-collapse-toggle" id="rpg-collapse-toggle" title="Collapse/Expand Panel">
|
<button class="rpg-collapse-toggle" id="rpg-collapse-toggle" title="Collapse/Expand Panel" data-i18n-title="global.collapseExpandPanel">
|
||||||
<i class="fa-solid fa-chevron-right"></i>
|
<i class="fa-solid fa-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Strip Widget Container (shown when collapsed with strip widgets enabled) -->
|
||||||
|
<div id="rpg-strip-widget-container" class="rpg-strip-widget-container">
|
||||||
|
<!-- Weather Icon Widget -->
|
||||||
|
<div class="rpg-strip-widget rpg-strip-widget-weather" data-widget="weatherIcon">
|
||||||
|
<span class="rpg-strip-widget-icon"></span>
|
||||||
|
<span class="rpg-strip-widget-desc"></span>
|
||||||
|
</div>
|
||||||
|
<!-- Clock Widget with animated face -->
|
||||||
|
<div class="rpg-strip-widget rpg-strip-widget-clock" data-widget="clock">
|
||||||
|
<div class="rpg-strip-clock-face">
|
||||||
|
<div class="rpg-strip-clock-hour"></div>
|
||||||
|
<div class="rpg-strip-clock-minute"></div>
|
||||||
|
<div class="rpg-strip-clock-center"></div>
|
||||||
|
</div>
|
||||||
|
<span class="rpg-strip-widget-value"></span>
|
||||||
|
</div>
|
||||||
|
<!-- Date Widget -->
|
||||||
|
<div class="rpg-strip-widget rpg-strip-widget-date" data-widget="date">
|
||||||
|
<i class="fa-solid fa-calendar"></i>
|
||||||
|
<span class="rpg-strip-widget-value"></span>
|
||||||
|
</div>
|
||||||
|
<!-- Location Widget -->
|
||||||
|
<div class="rpg-strip-widget rpg-strip-widget-location" data-widget="location">
|
||||||
|
<i class="fa-solid fa-location-dot"></i>
|
||||||
|
<span class="rpg-strip-widget-value"></span>
|
||||||
|
</div>
|
||||||
|
<!-- Stats Widget -->
|
||||||
|
<div class="rpg-strip-widget rpg-strip-widget-stats" data-widget="stats">
|
||||||
|
<div class="rpg-strip-stats-list"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Attributes Widget -->
|
||||||
|
<div class="rpg-strip-widget rpg-strip-widget-attributes" data-widget="attributes">
|
||||||
|
<div class="rpg-strip-attributes-grid"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Refresh Button (bottom) -->
|
||||||
|
<button id="rpg-strip-refresh" class="rpg-strip-refresh-btn" title="Refresh RPG Info" data-i18n-title="global.refreshRpgInfo">
|
||||||
|
<i class="fa-solid fa-sync"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Main Game Panel -->
|
<!-- Main Game Panel -->
|
||||||
<div class="rpg-game-container">
|
<div class="rpg-game-container">
|
||||||
<!-- Header with Controls -->
|
<!-- Header with Controls -->
|
||||||
@@ -56,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 -->
|
||||||
@@ -74,7 +122,7 @@
|
|||||||
<div class="rpg-features-row" id="rpg-features-row">
|
<div class="rpg-features-row" id="rpg-features-row">
|
||||||
<!-- HTML Prompt Toggle -->
|
<!-- HTML Prompt Toggle -->
|
||||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-html-toggle-wrapper">
|
<div class="rpg-toggle-container rpg-feature-col" id="rpg-html-toggle-wrapper">
|
||||||
<label class="rpg-toggle-label" title="Immersive HTML">
|
<label class="rpg-toggle-label" title="Immersive HTML" data-i18n-title="template.mainPanel.immersiveHtml">
|
||||||
<input type="checkbox" id="rpg-toggle-html-prompt">
|
<input type="checkbox" id="rpg-toggle-html-prompt">
|
||||||
<i class="fa-solid fa-code"></i>
|
<i class="fa-solid fa-code"></i>
|
||||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.immersiveHtml">Immersive HTML</span>
|
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.immersiveHtml">Immersive HTML</span>
|
||||||
@@ -83,16 +131,43 @@
|
|||||||
|
|
||||||
<!-- Dialogue Coloring Toggle -->
|
<!-- Dialogue Coloring Toggle -->
|
||||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-dialogue-coloring-toggle-wrapper">
|
<div class="rpg-toggle-container rpg-feature-col" id="rpg-dialogue-coloring-toggle-wrapper">
|
||||||
<label class="rpg-toggle-label" title="Colored Dialogues">
|
<label class="rpg-toggle-label" title="Colored Dialogues" data-i18n-title="template.mainPanel.coloredDialogues">
|
||||||
<input type="checkbox" id="rpg-toggle-dialogue-coloring">
|
<input type="checkbox" id="rpg-toggle-dialogue-coloring">
|
||||||
<i class="fa-solid fa-palette"></i>
|
<i class="fa-solid fa-palette"></i>
|
||||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.coloredDialogues">Colored Dialogues</span>
|
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.coloredDialogues">Colored Dialogues</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Deception System Toggle -->
|
||||||
|
<div class="rpg-toggle-container rpg-feature-col" id="rpg-deception-toggle-wrapper">
|
||||||
|
<label class="rpg-toggle-label" title="Deception System" data-i18n-title="template.mainPanel.deceptionSystem">
|
||||||
|
<input type="checkbox" id="rpg-toggle-deception">
|
||||||
|
<i class="fa-solid fa-masks-theater"></i>
|
||||||
|
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.deceptionSystem">Deception System</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Omniscience Filter Toggle -->
|
||||||
|
<div class="rpg-toggle-container rpg-feature-col" id="rpg-omniscience-toggle-wrapper">
|
||||||
|
<label class="rpg-toggle-label" title="Omniscience Filter" data-i18n-title="template.mainPanel.omniscienceFilter">
|
||||||
|
<input type="checkbox" id="rpg-toggle-omniscience">
|
||||||
|
<i class="fa-solid fa-eye-slash"></i>
|
||||||
|
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.omniscienceFilter">Omniscience Filter</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CYOA Toggle -->
|
||||||
|
<div class="rpg-toggle-container rpg-feature-col" id="rpg-cyoa-toggle-wrapper">
|
||||||
|
<label class="rpg-toggle-label" title="CYOA" data-i18n-title="template.mainPanel.cyoa">
|
||||||
|
<input type="checkbox" id="rpg-toggle-cyoa">
|
||||||
|
<i class="fa-solid fa-list-ol"></i>
|
||||||
|
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.cyoa">CYOA</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Spotify Music Toggle -->
|
<!-- Spotify Music Toggle -->
|
||||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-spotify-toggle-wrapper">
|
<div class="rpg-toggle-container rpg-feature-col" id="rpg-spotify-toggle-wrapper">
|
||||||
<label class="rpg-toggle-label" title="Spotify Music">
|
<label class="rpg-toggle-label" title="Spotify Music" data-i18n-title="template.mainPanel.spotifyMusic">
|
||||||
<input type="checkbox" id="rpg-toggle-spotify-music">
|
<input type="checkbox" id="rpg-toggle-spotify-music">
|
||||||
<i class="fa-brands fa-spotify"></i>
|
<i class="fa-brands fa-spotify"></i>
|
||||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.spotifyMusic">Spotify Music</span>
|
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.spotifyMusic">Spotify Music</span>
|
||||||
@@ -101,7 +176,7 @@
|
|||||||
|
|
||||||
<!-- Dynamic Weather Toggle -->
|
<!-- Dynamic Weather Toggle -->
|
||||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-dynamic-weather-toggle-wrapper">
|
<div class="rpg-toggle-container rpg-feature-col" id="rpg-dynamic-weather-toggle-wrapper">
|
||||||
<label class="rpg-toggle-label" title="Dynamic Weather Effects">
|
<label class="rpg-toggle-label" title="Dynamic Weather Effects" data-i18n-title="template.mainPanel.dynamicWeatherEffects">
|
||||||
<input type="checkbox" id="rpg-toggle-dynamic-weather">
|
<input type="checkbox" id="rpg-toggle-dynamic-weather">
|
||||||
<i class="fa-solid fa-cloud-sun-rain"></i>
|
<i class="fa-solid fa-cloud-sun-rain"></i>
|
||||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.dynamicWeatherEffects">Dynamic Weather</span>
|
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.dynamicWeatherEffects">Dynamic Weather</span>
|
||||||
@@ -109,7 +184,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Narrator Mode Toggle -->
|
<!-- Narrator Mode Toggle -->
|
||||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-narrator-toggle-wrapper">
|
<div class="rpg-toggle-container rpg-feature-col" id="rpg-narrator-toggle-wrapper">
|
||||||
<label class="rpg-toggle-label" title="Narrator Mode">
|
<label class="rpg-toggle-label" title="Narrator Mode" data-i18n-title="template.mainPanel.narratorMode">
|
||||||
<input type="checkbox" id="rpg-toggle-narrator">
|
<input type="checkbox" id="rpg-toggle-narrator">
|
||||||
<i class="fa-solid fa-book-open"></i>
|
<i class="fa-solid fa-book-open"></i>
|
||||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.narratorMode">Narrator Mode</span>
|
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.narratorMode">Narrator Mode</span>
|
||||||
@@ -117,7 +192,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- Auto-generate Avatars Toggle -->
|
<!-- Auto-generate Avatars Toggle -->
|
||||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-auto-avatars-toggle-wrapper">
|
<div class="rpg-toggle-container rpg-feature-col" id="rpg-auto-avatars-toggle-wrapper">
|
||||||
<label class="rpg-toggle-label" title="Auto-generate Avatars">
|
<label class="rpg-toggle-label" title="Auto-generate Avatars" data-i18n-title="template.mainPanel.autoAvatars">
|
||||||
<input type="checkbox" id="rpg-toggle-auto-avatars-panel">
|
<input type="checkbox" id="rpg-toggle-auto-avatars-panel">
|
||||||
<i class="fa-solid fa-user-plus"></i>
|
<i class="fa-solid fa-user-plus"></i>
|
||||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.autoAvatars">Auto Avatars</span>
|
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.autoAvatars">Auto Avatars</span>
|
||||||
@@ -145,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">
|
||||||
@@ -183,29 +294,49 @@
|
|||||||
<div class="rpg-setting-row">
|
<div class="rpg-setting-row">
|
||||||
<label for="rpg-custom-bg"
|
<label for="rpg-custom-bg"
|
||||||
data-i18n-key="template.settingsModal.themeOptions.custom.background">Background:</label>
|
data-i18n-key="template.settingsModal.themeOptions.custom.background">Background:</label>
|
||||||
<input type="color" id="rpg-custom-bg" value="#1a1a2e" />
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<input type="color" id="rpg-custom-bg" value="#1a1a2e" style="width: 60px;" />
|
||||||
|
<input type="range" id="rpg-custom-bg-opacity" min="0" max="100" value="100" style="flex: 1;" />
|
||||||
|
<span id="rpg-custom-bg-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-setting-row">
|
<div class="rpg-setting-row">
|
||||||
<label for="rpg-custom-accent"
|
<label for="rpg-custom-accent"
|
||||||
data-i18n-key="template.settingsModal.themeOptions.custom.accent">Accent:</label>
|
data-i18n-key="template.settingsModal.themeOptions.custom.accent">Accent:</label>
|
||||||
<input type="color" id="rpg-custom-accent" value="#16213e" />
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<input type="color" id="rpg-custom-accent" value="#16213e" style="width: 60px;" />
|
||||||
|
<input type="range" id="rpg-custom-accent-opacity" min="0" max="100" value="100" style="flex: 1;" />
|
||||||
|
<span id="rpg-custom-accent-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-setting-row">
|
<div class="rpg-setting-row">
|
||||||
<label for="rpg-custom-text"
|
<label for="rpg-custom-text"
|
||||||
data-i18n-key="template.settingsModal.themeOptions.custom.text">Text:</label>
|
data-i18n-key="template.settingsModal.themeOptions.custom.text">Text:</label>
|
||||||
<input type="color" id="rpg-custom-text" value="#eaeaea" />
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<input type="color" id="rpg-custom-text" value="#eaeaea" style="width: 60px;" />
|
||||||
|
<input type="range" id="rpg-custom-text-opacity" min="0" max="100" value="100" style="flex: 1;" />
|
||||||
|
<span id="rpg-custom-text-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-setting-row">
|
<div class="rpg-setting-row">
|
||||||
<label for="rpg-custom-highlight"
|
<label for="rpg-custom-highlight"
|
||||||
data-i18n-key="template.settingsModal.themeOptions.custom.highlight">Highlight:</label>
|
data-i18n-key="template.settingsModal.themeOptions.custom.highlight">Highlight:</label>
|
||||||
<input type="color" id="rpg-custom-highlight" value="#e94560" />
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<input type="color" id="rpg-custom-highlight" value="#e94560" style="width: 60px;" />
|
||||||
|
<input type="range" id="rpg-custom-highlight-opacity" min="0" max="100" value="100" style="flex: 1;" />
|
||||||
|
<span id="rpg-custom-highlight-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rpg-setting-row">
|
<div class="rpg-setting-row">
|
||||||
<label for="rpg-stat-bar-color-low" data-i18n-key="template.settingsModal.theme.statBarLow">Stat Bar
|
<label for="rpg-stat-bar-color-low" data-i18n-key="template.settingsModal.theme.statBarLow">Stat Bar
|
||||||
Color (Low):</label>
|
Color (Low):</label>
|
||||||
<input type="color" id="rpg-stat-bar-color-low" value="#cc3333" />
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<input type="color" id="rpg-stat-bar-color-low" value="#cc3333" style="width: 60px;" />
|
||||||
|
<input type="range" id="rpg-stat-bar-color-low-opacity" min="0" max="100" value="100" style="flex: 1;" />
|
||||||
|
<span id="rpg-stat-bar-color-low-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
|
||||||
|
</div>
|
||||||
<small data-i18n-key="template.settingsModal.theme.statBarLowNote">Color when stats are at
|
<small data-i18n-key="template.settingsModal.theme.statBarLowNote">Color when stats are at
|
||||||
0%.</small>
|
0%.</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,7 +344,11 @@
|
|||||||
<div class="rpg-setting-row">
|
<div class="rpg-setting-row">
|
||||||
<label for="rpg-stat-bar-color-high" data-i18n-key="template.settingsModal.theme.statBarHigh">Stat
|
<label for="rpg-stat-bar-color-high" data-i18n-key="template.settingsModal.theme.statBarHigh">Stat
|
||||||
Bar Color (High):</label>
|
Bar Color (High):</label>
|
||||||
<input type="color" id="rpg-stat-bar-color-high" value="#33cc66" />
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<input type="color" id="rpg-stat-bar-color-high" value="#33cc66" style="width: 60px;" />
|
||||||
|
<input type="range" id="rpg-stat-bar-color-high-opacity" min="0" max="100" value="100" style="flex: 1;" />
|
||||||
|
<span id="rpg-stat-bar-color-high-opacity-value" style="min-width: 35px; text-align: right;">100%</span>
|
||||||
|
</div>
|
||||||
<small data-i18n-key="template.settingsModal.theme.statBarHighNote">Color when stats are at
|
<small data-i18n-key="template.settingsModal.theme.statBarHighNote">Color when stats are at
|
||||||
100%.</small>
|
100%.</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -267,6 +402,33 @@
|
|||||||
Display character portraits with their current thoughts and status.
|
Display character portraits with their current thoughts and status.
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-alt-present-characters" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.showBelowChatPresentCharacters">Show Below-Chat Present Characters</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.showBelowChatPresentCharactersNote">
|
||||||
|
Display a compact Present Characters panel below the chat.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-thought-based-expressions" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.thoughtBasedExpressions">Thought-Based Expressions</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.thoughtBasedExpressionsNote">
|
||||||
|
Use SillyTavern Character Expressions to classify each present character's thoughts for the below-chat panel. May increase token usage depending on the selected Classifier API.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-hide-default-expressions" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.hideDefaultExpressionDisplay">Hide Default Expression Display</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.hideDefaultExpressionDisplayNote">
|
||||||
|
Hide SillyTavern's built-in Character Expressions display.
|
||||||
|
</small>
|
||||||
|
|
||||||
<label class="checkbox_label">
|
<label class="checkbox_label">
|
||||||
<input type="checkbox" id="rpg-toggle-thoughts-in-chat" />
|
<input type="checkbox" id="rpg-toggle-thoughts-in-chat" />
|
||||||
<span data-i18n-key="template.settingsModal.display.showThoughtsInChat">Show Thoughts</span>
|
<span data-i18n-key="template.settingsModal.display.showThoughtsInChat">Show Thoughts</span>
|
||||||
@@ -276,6 +438,15 @@
|
|||||||
Display character thoughts as overlay bubbles next to their messages.
|
Display character thoughts as overlay bubbles next to their messages.
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-inline-thoughts" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.showInlineThoughts">Show Thoughts Below Message Text</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.showInlineThoughtsNote">
|
||||||
|
Switch between the default corner thought bubbles and thought cards below the message text.
|
||||||
|
</small>
|
||||||
|
|
||||||
<label class="checkbox_label">
|
<label class="checkbox_label">
|
||||||
<input type="checkbox" id="rpg-toggle-inventory" />
|
<input type="checkbox" id="rpg-toggle-inventory" />
|
||||||
<span data-i18n-key="template.settingsModal.display.showInventory">Show Inventory</span>
|
<span data-i18n-key="template.settingsModal.display.showInventory">Show Inventory</span>
|
||||||
@@ -285,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>
|
||||||
@@ -321,6 +501,33 @@
|
|||||||
Display a toggle button to enable/disable colored dialogue formatting.
|
Display a toggle button to enable/disable colored dialogue formatting.
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-show-deception-toggle" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.showDeceptionToggle">Show Deception System</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.showDeceptionToggleNote">
|
||||||
|
Display a toggle button to enable/disable special formatting of lies and deceptions crafted by the model, allowing it to easily track whenever one was committed, without showing it to the user.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-show-omniscience-toggle" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.showOmniscienceToggle">Show Omniscience Filter</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.showOmniscienceToggleNote">
|
||||||
|
Display a toggle button to enable/disable the omniscience filter, which instructs the AI to hide information the player character cannot perceive (events behind them, in other rooms, etc.) in special tags.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-show-cyoa-toggle" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.showCYOAToggle">Show CYOA</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.showCYOAToggleNote">
|
||||||
|
Display a toggle button to enable/disable "Choose Your Own Adventure" formatting instruction that makes the model produce five possible actions/dialogues for you to choose from at the end of the output.
|
||||||
|
</small>
|
||||||
|
|
||||||
<label class="checkbox_label">
|
<label class="checkbox_label">
|
||||||
<input type="checkbox" id="rpg-toggle-show-spotify-toggle" />
|
<input type="checkbox" id="rpg-toggle-show-spotify-toggle" />
|
||||||
<span data-i18n-key="template.settingsModal.display.showSpotifyMusicToggle">Show Spotify Music</span>
|
<span data-i18n-key="template.settingsModal.display.showSpotifyMusicToggle">Show Spotify Music</span>
|
||||||
@@ -339,6 +546,27 @@
|
|||||||
Display a toggle button to enable/disable animated weather effects.
|
Display a toggle button to enable/disable animated weather effects.
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
|
<!-- Weather sub-options (shown when dynamic weather is enabled) -->
|
||||||
|
<div id="rpg-weather-suboptions" style="margin-left: 24px; margin-top: 8px;">
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="radio" name="rpg-weather-position" id="rpg-toggle-weather-background" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.weatherPosition.background">Show in Background</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.weatherPosition.backgroundNote">
|
||||||
|
Display weather effects behind the chat (standard behavior).
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="radio" name="rpg-weather-position" id="rpg-toggle-weather-foreground" />
|
||||||
|
<span data-i18n-key="template.settingsModal.display.weatherPosition.foreground">Show in Foreground</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.display.weatherPosition.foregroundNote">
|
||||||
|
Display weather effects in front of the chat (experimental).
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="checkbox_label">
|
<label class="checkbox_label">
|
||||||
<input type="checkbox" id="rpg-toggle-show-narrator-mode" />
|
<input type="checkbox" id="rpg-toggle-show-narrator-mode" />
|
||||||
<span data-i18n-key="template.settingsModal.display.showNarratorMode">Show Narrator Mode</span>
|
<span data-i18n-key="template.settingsModal.display.showNarratorMode">Show Narrator Mode</span>
|
||||||
@@ -395,6 +623,111 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mobile FAB Options Section -->
|
||||||
|
<div class="rpg-settings-group">
|
||||||
|
<h4 data-i18n-key="template.settingsModal.mobileFabTitle">Mobile Button Widgets</h4>
|
||||||
|
<small class="notes" style="display: block; margin-bottom: 10px;"
|
||||||
|
data-i18n-key="template.settingsModal.mobileFabNote">
|
||||||
|
Show compact info widgets around the floating button on mobile. Widgets are positioned automatically.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-fab-widgets-enabled" />
|
||||||
|
<span data-i18n-key="template.settingsModal.mobileFab.enabled">Enable Floating Mobile Widgets</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.mobileFab.enabledNote">
|
||||||
|
Master toggle to show info widgets around the mobile floating button.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<div id="rpg-fab-widget-options" style="margin-left: 10px; border-left: 2px solid var(--SmartThemeBorderColor); padding-left: 10px; margin-top: 8px;">
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-fab-weather-icon" />
|
||||||
|
<span data-i18n-key="template.settingsModal.mobileFab.weatherIcon">Weather Icon</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-fab-weather-desc" />
|
||||||
|
<span data-i18n-key="template.settingsModal.mobileFab.weatherDesc">Weather Description</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-fab-clock" />
|
||||||
|
<span data-i18n-key="template.settingsModal.mobileFab.clock">Time/Clock</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-fab-date" />
|
||||||
|
<span data-i18n-key="template.settingsModal.mobileFab.date">Date</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-fab-location" />
|
||||||
|
<span data-i18n-key="template.settingsModal.mobileFab.location">Location</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-fab-stats" />
|
||||||
|
<span data-i18n-key="template.settingsModal.mobileFab.stats">Stats (Health, Energy, etc.)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-fab-attributes" />
|
||||||
|
<span data-i18n-key="template.settingsModal.mobileFab.attributes">RPG Attributes (STR, DEX, etc.)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Strip Widgets Section -->
|
||||||
|
<div class="rpg-settings-group">
|
||||||
|
<h4 data-i18n-key="template.settingsModal.desktopStripTitle">Desktop Collapsed Strip Widgets</h4>
|
||||||
|
<small class="notes" style="display: block; margin-bottom: 10px;"
|
||||||
|
data-i18n-key="template.settingsModal.desktopStripNote">
|
||||||
|
Show compact info widgets in the collapsed panel strip on desktop. Displays stats vertically without needing to expand the panel.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-widgets-enabled" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.enabled">Enable Strip Widgets</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.settingsModal.desktopStrip.enabledNote">
|
||||||
|
Shows widgets in the collapsed panel strip for quick access to stats.
|
||||||
|
</small>
|
||||||
|
|
||||||
|
<div id="rpg-strip-widget-options" style="margin-left: 10px; border-left: 2px solid var(--SmartThemeBorderColor); padding-left: 10px; margin-top: 8px; display: none;">
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-weather-icon" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.weatherIcon">Weather Icon</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-clock" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.clock">Time/Clock</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-date" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.date">Date</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-location" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.location">Location</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-stats" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.stats">Stats (Health, Energy, etc.)</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-toggle-strip-attributes" />
|
||||||
|
<span data-i18n-key="template.settingsModal.desktopStrip.attributes">RPG Attributes (STR, DEX, etc.)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="rpg-settings-group">
|
<div class="rpg-settings-group">
|
||||||
<h4 data-i18n-key="template.settingsModal.advancedTitle"><i class="fa-solid fa-sliders"
|
<h4 data-i18n-key="template.settingsModal.advancedTitle"><i class="fa-solid fa-sliders"
|
||||||
aria-hidden="true"></i> Advanced</h4>
|
aria-hidden="true"></i> Advanced</h4>
|
||||||
@@ -442,7 +775,7 @@
|
|||||||
<input type="password" id="rpg-external-api-key" class="rpg-input" placeholder="sk-..."
|
<input type="password" id="rpg-external-api-key" class="rpg-input" placeholder="sk-..."
|
||||||
style="flex: 1;" />
|
style="flex: 1;" />
|
||||||
<button id="rpg-toggle-api-key-visibility" class="menu_button" type="button"
|
<button id="rpg-toggle-api-key-visibility" class="menu_button" type="button"
|
||||||
title="Show/Hide API Key" style="padding: 4px 8px;">
|
title="Show/Hide API Key" data-i18n-title="global.showHideApiKey" style="padding: 4px 8px;">
|
||||||
<i class="fa-solid fa-eye"></i>
|
<i class="fa-solid fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -506,19 +839,7 @@
|
|||||||
Automatically refresh RPG info after each message.
|
Automatically refresh RPG info after each message.
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
<label class="checkbox_label" style="margin-top: 16px;">
|
<div class="rpg-setting-row" style="margin-top: 16px;">
|
||||||
<input type="checkbox" id="rpg-save-tracker-history" />
|
|
||||||
<span data-i18n-key="template.settingsModal.advanced.saveTrackerHistory">Save Tracker History in
|
|
||||||
Chat</span>
|
|
||||||
</label>
|
|
||||||
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;"
|
|
||||||
data-i18n-key="template.settingsModal.advanced.saveTrackerHistoryNote">
|
|
||||||
When enabled, tracker data is saved in chat history for each message. In Together mode, trackers
|
|
||||||
appear in <trackers> XML tags (hidden from display). In Separate mode, tracker data is stored
|
|
||||||
in message metadata. When disabled, only the most recent trackers are kept.
|
|
||||||
</small>
|
|
||||||
|
|
||||||
<div class="rpg-setting-row">
|
|
||||||
<label for="rpg-encounter-history-depth" data-i18n-key="template.settingsModal.advanced.encounterHistoryDepth">Chat History Depth For Encounters:</label>
|
<label for="rpg-encounter-history-depth" data-i18n-key="template.settingsModal.advanced.encounterHistoryDepth">Chat History Depth For Encounters:</label>
|
||||||
<input type="number" id="rpg-encounter-history-depth" min="1" max="20" value="8"
|
<input type="number" id="rpg-encounter-history-depth" min="1" max="20" value="8"
|
||||||
class="rpg-input" />
|
class="rpg-input" />
|
||||||
@@ -559,9 +880,10 @@
|
|||||||
<!-- Customize Prompts Button -->
|
<!-- Customize Prompts Button -->
|
||||||
<div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--rpg-border);">
|
<div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--rpg-border);">
|
||||||
<button id="rpg-open-prompts-editor" class="rpg-btn-customize-prompts">
|
<button id="rpg-open-prompts-editor" class="rpg-btn-customize-prompts">
|
||||||
<i class="fa-solid fa-file-lines" aria-hidden="true"></i> <span>Customize Prompts</span>
|
<i class="fa-solid fa-file-lines" aria-hidden="true"></i> <span data-i18n-key="template.promptsEditor.button">Customize Prompts</span>
|
||||||
</button>
|
</button>
|
||||||
<small style="display: block; margin-top: 8px; color: #888; font-size: 11px;">
|
<small style="display: block; margin-top: 8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.promptsEditor.buttonNote">
|
||||||
Edit all AI prompts used for generation, plot progression, and combat encounters.
|
Edit all AI prompts used for generation, plot progression, and combat encounters.
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -588,7 +910,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<small style="display: block; margin-top: 8px; color: #888; font-size: 11px;"
|
<small style="display: block; margin-top: 8px; color: #888; font-size: 11px;"
|
||||||
data-i18n-key="template.settingsModal.advanced.clearCacheNote">
|
data-i18n-key="template.settingsModal.advanced.clearCacheNote">
|
||||||
Clears all cached data including tracker history and temporary files.
|
Clears committed and displayed tracker data for your currently active chat.
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -602,9 +924,9 @@
|
|||||||
<header class="rpg-dice-popup-header">
|
<header class="rpg-dice-popup-header">
|
||||||
<h3 id="rpg-dice-title">
|
<h3 id="rpg-dice-title">
|
||||||
<i class="fa-solid fa-dice-d20" aria-hidden="true"></i>
|
<i class="fa-solid fa-dice-d20" aria-hidden="true"></i>
|
||||||
<span>Roll Dice</span>
|
<span data-i18n-key="dice.title">Roll Dice</span>
|
||||||
</h3>
|
</h3>
|
||||||
<button id="rpg-dice-popup-close" class="rpg-btn-icon" type="button" aria-label="Close dialog">
|
<button id="rpg-dice-popup-close" class="rpg-btn-icon" type="button" aria-label="Close dialog" data-i18n-aria-label="global.closeDialog">
|
||||||
<i class="fa-solid fa-times" aria-hidden="true"></i>
|
<i class="fa-solid fa-times" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
@@ -613,12 +935,12 @@
|
|||||||
<div class="rpg-dice-selector-container">
|
<div class="rpg-dice-selector-container">
|
||||||
<div class="rpg-dice-selector">
|
<div class="rpg-dice-selector">
|
||||||
<div class="rpg-dice-input-group">
|
<div class="rpg-dice-input-group">
|
||||||
<label for="rpg-dice-count">Number of Dice:</label>
|
<label for="rpg-dice-count" data-i18n-key="dice.numberOfDice">Number of Dice:</label>
|
||||||
<input type="number" id="rpg-dice-count" name="dice-count" min="1" max="20" value="1"
|
<input type="number" id="rpg-dice-count" name="dice-count" min="1" max="20" value="1"
|
||||||
class="rpg-input" />
|
class="rpg-input" />
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-dice-input-group">
|
<div class="rpg-dice-input-group">
|
||||||
<label for="rpg-dice-sides">Dice Type:</label>
|
<label for="rpg-dice-sides" data-i18n-key="dice.diceType">Dice Type:</label>
|
||||||
<select id="rpg-dice-sides" name="dice-sides" class="rpg-select">
|
<select id="rpg-dice-sides" name="dice-sides" class="rpg-select">
|
||||||
<option value="4">d4</option>
|
<option value="4">d4</option>
|
||||||
<option value="6">d6</option>
|
<option value="6">d6</option>
|
||||||
@@ -632,7 +954,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button id="rpg-dice-roll-btn" class="rpg-btn-primary" type="button">
|
<button id="rpg-dice-roll-btn" class="rpg-btn-primary" type="button">
|
||||||
<i class="fa-solid fa-dice" aria-hidden="true"></i>
|
<i class="fa-solid fa-dice" aria-hidden="true"></i>
|
||||||
<span>Roll Dice</span>
|
<span data-i18n-key="dice.title">Roll Dice</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -640,17 +962,17 @@
|
|||||||
<div class="rpg-dice-rolling">
|
<div class="rpg-dice-rolling">
|
||||||
<i class="fa-solid fa-dice-d20 fa-spin" aria-hidden="true"></i>
|
<i class="fa-solid fa-dice-d20 fa-spin" aria-hidden="true"></i>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-dice-rolling-text">Rolling...</div>
|
<div class="rpg-dice-rolling-text" data-i18n-key="dice.rolling">Rolling...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="rpg-dice-result" class="rpg-dice-result" hidden aria-live="polite">
|
<div id="rpg-dice-result" class="rpg-dice-result" hidden aria-live="polite">
|
||||||
<div class="rpg-dice-result-label">Result:</div>
|
<div class="rpg-dice-result-label" data-i18n-key="dice.result">Result:</div>
|
||||||
<output id="rpg-dice-result-value" class="rpg-dice-result-value"
|
<output id="rpg-dice-result-value" class="rpg-dice-result-value"
|
||||||
for="rpg-dice-count rpg-dice-sides">0</output>
|
for="rpg-dice-count rpg-dice-sides">0</output>
|
||||||
<div id="rpg-dice-result-details" class="rpg-dice-result-details" role="status"></div>
|
<div id="rpg-dice-result-details" class="rpg-dice-result-details" role="status"></div>
|
||||||
<button id="rpg-dice-save-btn" class="rpg-btn-primary rpg-dice-save-btn" type="button">
|
<button id="rpg-dice-save-btn" class="rpg-btn-primary rpg-dice-save-btn" type="button">
|
||||||
<i class="fa-solid fa-check" aria-hidden="true"></i>
|
<i class="fa-solid fa-check" aria-hidden="true"></i>
|
||||||
<span>Save Roll</span>
|
<span data-i18n-key="dice.saveRoll">Save Roll</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -670,6 +992,31 @@
|
|||||||
aria-label="Close tracker editor">×</button>
|
aria-label="Close tracker editor">×</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Preset Management Section -->
|
||||||
|
<div class="rpg-preset-management">
|
||||||
|
<div class="rpg-preset-row">
|
||||||
|
<label for="rpg-preset-select" data-i18n-key="preset.label">Preset:</label>
|
||||||
|
<select id="rpg-preset-select" class="rpg-select">
|
||||||
|
<!-- Options populated by JavaScript -->
|
||||||
|
</select>
|
||||||
|
<button id="rpg-preset-new" class="rpg-btn-icon" type="button" title="Create New Preset" data-i18n-title="preset.createNewPresetTitle">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
<button id="rpg-preset-default" class="rpg-btn-icon" type="button" title="Set as Default Preset" data-i18n-title="preset.setDefaultPresetTitle">
|
||||||
|
<i class="fa-solid fa-star"></i>
|
||||||
|
</button>
|
||||||
|
<button id="rpg-preset-delete" class="rpg-btn-icon" type="button" title="Delete Current Preset" data-i18n-title="preset.deleteCurrentPresetTitle">
|
||||||
|
<i class="fa-solid fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="rpg-preset-association-row">
|
||||||
|
<label class="checkbox_label">
|
||||||
|
<input type="checkbox" id="rpg-preset-associate">
|
||||||
|
<span data-i18n-key="preset.useThisPresetFor">Use this preset for: </span><strong id="rpg-preset-entity-name">Character</strong>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div class="rpg-editor-tabs">
|
<div class="rpg-editor-tabs">
|
||||||
<button class="rpg-editor-tab active" data-tab="userStats">
|
<button class="rpg-editor-tab active" data-tab="userStats">
|
||||||
@@ -684,6 +1031,10 @@
|
|||||||
<i class="fa-solid fa-users"></i> <span
|
<i class="fa-solid fa-users"></i> <span
|
||||||
data-i18n-key="template.trackerEditorModal.tabs.presentCharacters">Present Characters</span>
|
data-i18n-key="template.trackerEditorModal.tabs.presentCharacters">Present Characters</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="rpg-editor-tab" data-tab="historyPersistence">
|
||||||
|
<i class="fa-solid fa-clock-rotate-left"></i> <span
|
||||||
|
data-i18n-key="template.trackerEditorModal.tabs.historyPersistence">History Persistence</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rpg-settings-popup-body">
|
<div class="rpg-settings-popup-body">
|
||||||
@@ -691,6 +1042,7 @@
|
|||||||
<div id="rpg-editor-tab-userStats" class="rpg-editor-tab-content"></div>
|
<div id="rpg-editor-tab-userStats" class="rpg-editor-tab-content"></div>
|
||||||
<div id="rpg-editor-tab-infoBox" class="rpg-editor-tab-content" style="display: none;"></div>
|
<div id="rpg-editor-tab-infoBox" class="rpg-editor-tab-content" style="display: none;"></div>
|
||||||
<div id="rpg-editor-tab-presentCharacters" class="rpg-editor-tab-content" style="display: none;"></div>
|
<div id="rpg-editor-tab-presentCharacters" class="rpg-editor-tab-content" style="display: none;"></div>
|
||||||
|
<div id="rpg-editor-tab-historyPersistence" class="rpg-editor-tab-content" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="rpg-settings-popup-footer">
|
<footer class="rpg-settings-popup-footer">
|
||||||
@@ -725,162 +1077,218 @@
|
|||||||
<header class="rpg-settings-popup-header">
|
<header class="rpg-settings-popup-header">
|
||||||
<h3 id="rpg-prompts-editor-title">
|
<h3 id="rpg-prompts-editor-title">
|
||||||
<i class="fa-solid fa-file-lines" aria-hidden="true"></i>
|
<i class="fa-solid fa-file-lines" aria-hidden="true"></i>
|
||||||
<span>Customize Prompts</span>
|
<span data-i18n-key="template.promptsEditor.title">Customize Prompts</span>
|
||||||
</h3>
|
</h3>
|
||||||
<button id="rpg-close-prompts-editor" class="rpg-popup-close" type="button"
|
<button id="rpg-close-prompts-editor" class="rpg-popup-close" type="button"
|
||||||
aria-label="Close prompts editor">×</button>
|
aria-label="Close prompts editor">×</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="rpg-settings-popup-body">
|
<div class="rpg-settings-popup-body">
|
||||||
<small class="notes" style="display: block; margin-bottom: 16px;">
|
<small class="notes" style="display: block; margin-bottom: 16px;"
|
||||||
|
data-i18n-key="template.promptsEditor.description">
|
||||||
Customize the AI prompts used throughout the extension. Leave fields empty to use defaults.
|
Customize the AI prompts used throughout the extension. Leave fields empty to use defaults.
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
<!-- HTML Prompt -->
|
<!-- HTML Prompt -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-html" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-html" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-solid fa-code"></i> HTML Prompt
|
<i class="fa-solid fa-code"></i> <span data-i18n-key="template.promptsEditor.htmlPrompt.title">HTML Prompt</span>
|
||||||
</label>
|
</label>
|
||||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.promptsEditor.htmlPrompt.note">
|
||||||
Injected when "Enable Immersive HTML" is enabled. Affects all generation modes.
|
Injected when "Enable Immersive HTML" is enabled. Affects all generation modes.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-html" class="rpg-prompt-textarea" rows="4"></textarea>
|
<textarea id="rpg-prompt-html" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="html" style="margin-top: 8px;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="html" style="margin-top: 8px;">
|
||||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dialogue Coloring Prompt -->
|
<!-- Dialogue Coloring Prompt -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-dialogue-coloring" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-dialogue-coloring" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-solid fa-palette"></i> Dialogue Coloring Prompt
|
<i class="fa-solid fa-palette"></i> <span data-i18n-key="template.promptsEditor.dialogueColoringPrompt.title">Dialogue Coloring Prompt</span>
|
||||||
</label>
|
</label>
|
||||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;"
|
||||||
|
data-i18n-key="template.promptsEditor.dialogueColoringPrompt.note">
|
||||||
Injected when "Enable Dialogue Coloring" is enabled. Affects all generation modes.
|
Injected when "Enable Dialogue Coloring" is enabled. Affects all generation modes.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-dialogue-coloring" class="rpg-prompt-textarea" rows="4"></textarea>
|
<textarea id="rpg-prompt-dialogue-coloring" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="dialogue-coloring" style="margin-top: 8px;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="dialogue-coloring" style="margin-top: 8px;">
|
||||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Deception System Prompt -->
|
||||||
|
<div class="rpg-prompt-editor-section">
|
||||||
|
<label for="rpg-prompt-deception" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
|
<i class="fa-solid fa-masks-theater"></i> <span data-i18n-key="template.promptsEditor.deceptionPrompt.title">Deception System Prompt</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;" data-i18n-key="template.promptsEditor.deceptionPrompt.note">
|
||||||
|
Injected when "Enable Deception System" is enabled. Instructs AI to mark lies and deceptions with hidden tags.
|
||||||
|
</small>
|
||||||
|
<textarea id="rpg-prompt-deception" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||||
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="deception" style="margin-top: 8px;">
|
||||||
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Omniscience Filter Prompt -->
|
||||||
|
<div class="rpg-prompt-editor-section">
|
||||||
|
<label for="rpg-prompt-omniscience" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
|
<i class="fa-solid fa-eye-slash"></i> <span data-i18n-key="template.promptsEditor.omnisciencePrompt.title">Omniscience Filter Prompt</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;" data-i18n-key="template.promptsEditor.omnisciencePrompt.note">
|
||||||
|
Injected when "Enable Omniscience Filter" is enabled. Instructs AI to separate information the player character cannot perceive into hidden <ofilter> tags.
|
||||||
|
</small>
|
||||||
|
<textarea id="rpg-prompt-omniscience" class="rpg-prompt-textarea" rows="6"></textarea>
|
||||||
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="omniscience" style="margin-top: 8px;">
|
||||||
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CYOA Prompt -->
|
||||||
|
<div class="rpg-prompt-editor-section">
|
||||||
|
<label for="rpg-prompt-cyoa" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
|
<i class="fa-solid fa-list-ol"></i> <span data-i18n-key="template.promptsEditor.cyoaPrompt.title">CYOA Prompt</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;" data-i18n-key="template.promptsEditor.cyoaPrompt.note">
|
||||||
|
Injected when "Enable CYOA" is enabled. Instructs AI to end responses with numbered action choices. Uses very high priority (depth 102) to ensure it's the last instruction.
|
||||||
|
</small>
|
||||||
|
<textarea id="rpg-prompt-cyoa" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||||
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="cyoa" style="margin-top: 8px;">
|
||||||
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Spotify Music Prompt -->
|
<!-- Spotify Music Prompt -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-spotify" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-spotify" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-brands fa-spotify"></i> Spotify Music Prompt
|
<i class="fa-brands fa-spotify"></i> <span data-i18n-key="template.promptsEditor.spotifyPrompt.title">Spotify Music Prompt</span>
|
||||||
</label>
|
</label>
|
||||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;" data-i18n-key="template.promptsEditor.spotifyPrompt.note">
|
||||||
Injected when "Enable Spotify Music" is enabled. Asks AI to suggest appropriate music for the scene.
|
Injected when "Enable Spotify Music" is enabled. Asks AI to suggest appropriate music for the scene.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-spotify" class="rpg-prompt-textarea" rows="4"></textarea>
|
<textarea id="rpg-prompt-spotify" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="spotify" style="margin-top: 8px;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="spotify" style="margin-top: 8px;">
|
||||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Narrator Mode Prompt -->
|
<!-- Narrator Mode Prompt -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-narrator" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-narrator" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-solid fa-book-open"></i> Narrator Mode Prompt
|
<i class="fa-solid fa-book-open"></i> <span data-i18n-key="template.promptsEditor.narratorPrompt.title">Narrator Mode Prompt</span>
|
||||||
</label>
|
</label>
|
||||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;" data-i18n-key="template.promptsEditor.narratorPrompt.note">
|
||||||
Injected when "Narrator Mode" is enabled. Instructs AI to infer characters from context.
|
Injected when "Narrator Mode" is enabled. Instructs AI to infer characters from context.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-narrator" class="rpg-prompt-textarea" rows="3"></textarea>
|
<textarea id="rpg-prompt-narrator" class="rpg-prompt-textarea" rows="3"></textarea>
|
||||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="narrator" style="margin-top: 8px;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="narrator" style="margin-top: 8px;">
|
||||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Context Instructions Prompt -->
|
||||||
|
<div class="rpg-prompt-editor-section">
|
||||||
|
<label for="rpg-prompt-context-instructions" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
|
<i class="fa-solid fa-comment-dots"></i> <span data-i18n-key="template.promptsEditor.contextPrompt.title">Context Instructions Prompt</span>
|
||||||
|
</label>
|
||||||
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;" data-i18n-key="template.promptsEditor.contextPrompt.note">
|
||||||
|
Injected in Separate/External mode after the context summary. Tells the AI how to use the context.
|
||||||
|
</small>
|
||||||
|
<textarea id="rpg-prompt-context-instructions" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||||
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="contextInstructions" style="margin-top: 8px;">
|
||||||
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Random Plot Progression Prompt -->
|
<!-- Random Plot Progression Prompt -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-plot-random" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-plot-random" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-solid fa-dice"></i> Random Plot Progression Prompt
|
<i class="fa-solid fa-dice"></i> <span data-i18n-key="template.promptsEditor.randomPlotPrompt.title">Random Plot Progression Prompt</span>
|
||||||
</label>
|
</label>
|
||||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;" data-i18n-key="template.promptsEditor.randomPlotPrompt.note">
|
||||||
Injected when the "Randomized Plot" button is clicked. Introduces random elements to the story.
|
Injected when the "Randomized Plot" button is clicked. Introduces random elements to the story.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-plot-random" class="rpg-prompt-textarea" rows="6"></textarea>
|
<textarea id="rpg-prompt-plot-random" class="rpg-prompt-textarea" rows="6"></textarea>
|
||||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="plotRandom" style="margin-top: 8px;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="plotRandom" style="margin-top: 8px;">
|
||||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Natural Plot Progression Prompt -->
|
<!-- Natural Plot Progression Prompt -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-plot-natural" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-plot-natural" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-solid fa-forward"></i> Natural Plot Progression Prompt
|
<i class="fa-solid fa-forward"></i> <span data-i18n-key="template.promptsEditor.naturalPlotPrompt.title">Natural Plot Progression Prompt</span>
|
||||||
</label>
|
</label>
|
||||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;" data-i18n-key="template.promptsEditor.naturalPlotPrompt.note">
|
||||||
Injected when the "Natural Plot" button is clicked. Progresses the story naturally.
|
Injected when the "Natural Plot" button is clicked. Progresses the story naturally.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-plot-natural" class="rpg-prompt-textarea" rows="4"></textarea>
|
<textarea id="rpg-prompt-plot-natural" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="plotNatural" style="margin-top: 8px;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="plotNatural" style="margin-top: 8px;">
|
||||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Avatar Generation Instruction -->
|
<!-- Avatar Generation Instruction -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-avatar" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-avatar" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-solid fa-user-circle"></i> Avatar Generation Instruction
|
<i class="fa-solid fa-user-circle"></i> <span data-i18n-key="template.promptsEditor.avatarPrompt.title">Avatar Generation Instruction</span>
|
||||||
</label>
|
</label>
|
||||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;" data-i18n-key="template.promptsEditor.avatarPrompt.note">
|
||||||
Instructions for LLM when generating avatar image prompts. Used by Auto-generate Missing Avatars feature.
|
Instructions for LLM when generating avatar image prompts. Used by Auto-generate Missing Avatars feature.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-avatar" class="rpg-prompt-textarea" rows="3"></textarea>
|
<textarea id="rpg-prompt-avatar" class="rpg-prompt-textarea" rows="3"></textarea>
|
||||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="avatar" style="margin-top: 8px;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="avatar" style="margin-top: 8px;">
|
||||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tracker Instructions -->
|
<!-- Tracker Instructions -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-tracker-instructions" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-tracker-instructions" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-solid fa-list-check"></i> Tracker Instructions
|
<i class="fa-solid fa-list-check"></i> <span data-i18n-key="template.promptsEditor.trackerPrompt.title">Tracker Instructions</span>
|
||||||
</label>
|
</label>
|
||||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;" data-i18n-key="template.promptsEditor.trackerPrompt.note">
|
||||||
Instruction portion only (format specification is hardcoded). {userName} will be replaced with the user's name.
|
Instruction portion only (format specification is hardcoded). {userName} will be replaced with the user's name.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-tracker-instructions" class="rpg-prompt-textarea" rows="4"></textarea>
|
<textarea id="rpg-prompt-tracker-instructions" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="trackerInstructions" style="margin-top: 8px;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="trackerInstructions" style="margin-top: 8px;">
|
||||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tracker Continuation Instruction -->
|
<!-- Tracker Continuation Instruction -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-tracker-continuation" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-tracker-continuation" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-solid fa-arrow-right"></i> Tracker Continuation Instruction
|
<i class="fa-solid fa-arrow-right"></i> <span data-i18n-key="template.promptsEditor.trackerContinuationPrompt.title">Tracker Continuation Instruction</span>
|
||||||
</label>
|
</label>
|
||||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;" data-i18n-key="template.promptsEditor.trackerContinuationPrompt.note">
|
||||||
Instructions added after tracker format specifications, telling the AI how to continue the narrative.
|
Instructions added after tracker format specifications, telling the AI how to continue the narrative.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-tracker-continuation" class="rpg-prompt-textarea" rows="4"></textarea>
|
<textarea id="rpg-prompt-tracker-continuation" class="rpg-prompt-textarea" rows="4"></textarea>
|
||||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="trackerContinuation" style="margin-top: 8px;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="trackerContinuation" style="margin-top: 8px;">
|
||||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Combat Narrative Style Instruction -->
|
<!-- Combat Narrative Style Instruction -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-combat-narrative" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-combat-narrative" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-solid fa-fire"></i> Combat Narrative Style Instruction
|
<i class="fa-solid fa-fire"></i> <span data-i18n-key="template.promptsEditor.combatPrompt.title">Combat Narrative Style Instruction</span>
|
||||||
</label>
|
</label>
|
||||||
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;">
|
<small style="display: block; margin-bottom: 8px; color: #888; font-size: 11px;" data-i18n-key="template.promptsEditor.combatPrompt.note">
|
||||||
Writing style instructions for combat encounters. Includes prose quality guidelines and anti-repetition rules. {userName} will be replaced with the user's name.
|
Writing style instructions for combat encounters. Includes prose quality guidelines and anti-repetition rules. {userName} will be replaced with the user's name.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-combat-narrative" class="rpg-prompt-textarea" rows="6"></textarea>
|
<textarea id="rpg-prompt-combat-narrative" class="rpg-prompt-textarea" rows="6"></textarea>
|
||||||
<button class="menu_button rpg-restore-prompt-btn" data-prompt="combatNarrative" style="margin-top: 8px;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="combatNarrative" style="margin-top: 8px;">
|
||||||
<i class="fa-solid fa-rotate-left"></i> Restore Default
|
<i class="fa-solid fa-rotate-left"></i> <span data-i18n-key="template.promptsEditor.restoreDefault">Restore Default</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="rpg-settings-popup-footer">
|
<footer class="rpg-settings-popup-footer">
|
||||||
<button id="rpg-prompts-restore-all" class="rpg-btn-secondary" type="button">
|
|
||||||
<i class="fa-solid fa-rotate-left"></i> Restore All To Default
|
|
||||||
</button>
|
|
||||||
<div class="rpg-footer-right">
|
<div class="rpg-footer-right">
|
||||||
<button id="rpg-prompts-cancel" class="rpg-btn-secondary" type="button">Cancel</button>
|
<button id="rpg-prompts-cancel" class="rpg-btn-secondary" type="button">Cancel</button>
|
||||||
<button id="rpg-prompts-save" class="rpg-btn-primary" type="button">
|
<button id="rpg-prompts-save" class="rpg-btn-primary" type="button">
|
||||||
@@ -946,9 +1354,9 @@
|
|||||||
For the extension to work properly, **it is not recommended to use any models below 20B, especially if they're old.** It works best with the SOTA models such as Deepseek, Claude, GPT, or Gemini.
|
For the extension to work properly, **it is not recommended to use any models below 20B, especially if they're old.** It works best with the SOTA models such as Deepseek, Claude, GPT, or Gemini.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h4 style="margin-top: 20px; margin-bottom: 10px;"><strong>Special thanks to all the other contributors for this project:</strong></h4>
|
<h4 style="margin-top: 20px; margin-bottom: 10px;"><strong>Special thanks to all the contributors for this project:</strong></h4>
|
||||||
<p style="margin-left: 20px; line-height: 1.6;">
|
<p style="margin-left: 20px; line-height: 1.6;">
|
||||||
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude (???), IDeathByte, Chungchandev, Joenunezb, Amauragis, and Tomt610.
|
Paperboygold, Munimunigamer, Subarashimo, Lilminzyu, Claude (???), IDeathByte, Chungchandev, Joenunezb, Amauragis, Tomt610, and Olaroll.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="margin-top: 20px; text-align: center;">
|
<div style="margin-top: 20px; text-align: center;">
|
||||||
|
|||||||
Reference in New Issue
Block a user