Merge upstream/main into doom-lite-expression-sync-v2
This commit is contained in:
@@ -24,3 +24,4 @@ node_modules/
|
|||||||
|
|
||||||
# Claude
|
# Claude
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
yarn.lock
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import {
|
|||||||
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
|
||||||
@@ -850,6 +850,17 @@ async function initUI() {
|
|||||||
// 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;
|
||||||
}
|
}
|
||||||
|
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);
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -858,6 +869,17 @@ async function initUI() {
|
|||||||
if (!extensionSettings.enabled) {
|
if (!extensionSettings.enabled) {
|
||||||
return;
|
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);
|
await updateRPGData(renderUserStats, renderInfoBox, renderThoughts, renderInventory);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -6,6 +6,6 @@
|
|||||||
"js": "index.js",
|
"js": "index.js",
|
||||||
"css": "style.css",
|
"css": "style.css",
|
||||||
"author": "Marinara",
|
"author": "Marinara",
|
||||||
"version": "3.7.2",
|
"version": "3.7.3",
|
||||||
"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.3",
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
<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="ru" data-i18n-key="settings.language.option.ru">Русский</option>
|
||||||
<option value="fr" data-i18n-key="settings.language.option.fr">Français</option>
|
<option value="fr" data-i18n-key="settings.language.option.fr">Français</option>
|
||||||
|
|||||||
+138
-1
@@ -480,6 +480,26 @@ export function saveChatData() {
|
|||||||
saveChatDebounced();
|
saveChatDebounced();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors a tracker data entry into message.swipe_info so it survives page reloads.
|
||||||
|
* ST only serializes swipe_info to disk; message.extra is in-memory only.
|
||||||
|
* Guard: skips silently if swipe_info[swipeId] doesn't exist yet
|
||||||
|
*
|
||||||
|
* @param {Object} message - The chat message object
|
||||||
|
* @param {number} swipeId - The swipe index to mirror into
|
||||||
|
* @param {Object} swipeEntry - { userStats, infoBox, characterThoughts }
|
||||||
|
*/
|
||||||
|
export function mirrorToSwipeInfo(message, swipeId, swipeEntry) {
|
||||||
|
if (!message.swipe_info || !message.swipe_info[swipeId]) return;
|
||||||
|
if (!message.swipe_info[swipeId].extra) {
|
||||||
|
message.swipe_info[swipeId].extra = {};
|
||||||
|
}
|
||||||
|
if (!message.swipe_info[swipeId].extra.rpg_companion_swipes) {
|
||||||
|
message.swipe_info[swipeId].extra.rpg_companion_swipes = {};
|
||||||
|
}
|
||||||
|
message.swipe_info[swipeId].extra.rpg_companion_swipes[swipeId] = swipeEntry;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the last assistant message's swipe data with current tracker data.
|
* Updates the last assistant message's swipe data with current tracker data.
|
||||||
* This ensures user edits are preserved across swipes and included in generation context.
|
* This ensures user edits are preserved across swipes and included in generation context.
|
||||||
@@ -493,7 +513,7 @@ export function updateMessageSwipeData() {
|
|||||||
// 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 - update its swipe data
|
// Found last assistant message - update its swipe data
|
||||||
if (!message.extra) {
|
if (!message.extra) {
|
||||||
message.extra = {};
|
message.extra = {};
|
||||||
@@ -515,6 +535,123 @@ export function updateMessageSwipeData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads RPG tracker data for a specific swipe from a message.
|
||||||
|
* Checks message.extra first (in-memory, current session), then message.swipe_info
|
||||||
|
* (serialized by SillyTavern on save, available after page reload).
|
||||||
|
*
|
||||||
|
* @param {Object} message - The chat message object
|
||||||
|
* @param {number} swipeId - The swipe index to read
|
||||||
|
* @returns {{userStats, infoBox, characterThoughts}|null} The swipe data or null
|
||||||
|
*/
|
||||||
|
export function getSwipeData(message, swipeId) {
|
||||||
|
// Primary: in-memory extra (current session or after a recent write)
|
||||||
|
const fromExtra = message.extra?.rpg_companion_swipes?.[swipeId];
|
||||||
|
if (fromExtra) return fromExtra;
|
||||||
|
|
||||||
|
// Fallback: swipe_info (populated by ST when loading from disk)
|
||||||
|
const fromSwipeInfo = message.swipe_info?.[swipeId]?.extra?.rpg_companion_swipes?.[swipeId];
|
||||||
|
if (fromSwipeInfo) return fromSwipeInfo;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits tracker data from the assistant message immediately before currentMessageIndex.
|
||||||
|
* Walks backward through the chat skipping the current message, user messages, and system
|
||||||
|
* messages until it finds the prior assistant message, then loads its active swipe data.
|
||||||
|
* If no prior assistant message exists or exists without a tracker state, nulls out all fields so
|
||||||
|
* the AI generates from an empty context rather than a ghost state.
|
||||||
|
*
|
||||||
|
* @param {number} currentMessageIndex - Index of the message to start searching before
|
||||||
|
*/
|
||||||
|
export function commitTrackerDataFromPriorMessage(currentMessageIndex) {
|
||||||
|
const chat = getContext().chat;
|
||||||
|
if (!chat || chat.length === 0) {
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('[RPG Companion] commitTrackerDataFromPriorMessage called with index', currentMessageIndex, '| chat.length =', chat.length);
|
||||||
|
|
||||||
|
for (let i = currentMessageIndex - 1; i >= 0; i--) {
|
||||||
|
const message = chat[i];
|
||||||
|
if (message.is_user || message.is_system) continue;
|
||||||
|
|
||||||
|
// Found the prior assistant message — commit its active swipe data
|
||||||
|
const swipeId = message.swipe_id || 0;
|
||||||
|
const swipeData = getSwipeData(message, swipeId);
|
||||||
|
// console.log('[RPG Companion] Committing from chat[' + i + '] swipe', swipeId, '| has swipe data:', !!swipeData);
|
||||||
|
committedTrackerData.userStats = swipeData?.userStats || null;
|
||||||
|
committedTrackerData.infoBox = swipeData?.infoBox || null;
|
||||||
|
const rawCharacterThoughts = swipeData?.characterThoughts;
|
||||||
|
committedTrackerData.characterThoughts =
|
||||||
|
rawCharacterThoughts == null
|
||||||
|
? null
|
||||||
|
: (typeof rawCharacterThoughts === 'string'
|
||||||
|
? rawCharacterThoughts
|
||||||
|
: JSON.stringify(rawCharacterThoughts));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No prior assistant message found — use empty context
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates a message's current swipe slot with tracker data inherited from the
|
||||||
|
* nearest prior assistant message, when no tracker data has been generated for
|
||||||
|
* this swipe yet (e.g. auto-update is disabled).
|
||||||
|
*
|
||||||
|
* This ensures that commitTrackerDataFromPriorMessage can always find a tracker
|
||||||
|
* state to commit when the user sends the next message, rather than nulling
|
||||||
|
* everything out and resetting the tracker display to empty.
|
||||||
|
*
|
||||||
|
* Does nothing if the current swipe already has its own tracker data.
|
||||||
|
*
|
||||||
|
* @param {Object} message - The assistant message object to inherit into
|
||||||
|
* @param {number} messageIndex - Index of that message in chat
|
||||||
|
* @returns {boolean} True if inheritance was written, false otherwise
|
||||||
|
*/
|
||||||
|
export function inheritSwipeDataFromPriorMessage(message, messageIndex) {
|
||||||
|
const chat = getContext().chat;
|
||||||
|
if (!chat) return false;
|
||||||
|
|
||||||
|
const currentSwipeId = message.swipe_id || 0;
|
||||||
|
|
||||||
|
// Don't overwrite if this swipe already has its own tracker data.
|
||||||
|
if (getSwipeData(message, currentSwipeId)) return false;
|
||||||
|
|
||||||
|
// Walk backward to find the nearest prior assistant message with swipe data.
|
||||||
|
for (let i = messageIndex - 1; i >= 0; i--) {
|
||||||
|
const msg = chat[i];
|
||||||
|
if (msg.is_user || msg.is_system) continue;
|
||||||
|
|
||||||
|
const swipeId = msg.swipe_id || 0;
|
||||||
|
const swipeData = getSwipeData(msg, swipeId);
|
||||||
|
if (!swipeData) continue; // No data on this assistant message; keep searching further back
|
||||||
|
|
||||||
|
// Write inherited data into this swipe slot.
|
||||||
|
if (!message.extra) message.extra = {};
|
||||||
|
if (!message.extra.rpg_companion_swipes) message.extra.rpg_companion_swipes = {};
|
||||||
|
|
||||||
|
const inherited = {
|
||||||
|
userStats: swipeData.userStats,
|
||||||
|
infoBox: swipeData.infoBox,
|
||||||
|
characterThoughts: swipeData.characterThoughts
|
||||||
|
};
|
||||||
|
message.extra.rpg_companion_swipes[currentSwipeId] = inherited;
|
||||||
|
mirrorToSwipeInfo(message, currentSwipeId, inherited);
|
||||||
|
// console.log('[RPG Companion] Inherited tracker data from chat[' + i + '] into current swipe slot', currentSwipeId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads RPG data from the current chat's metadata.
|
* Loads RPG data from the current chat's metadata.
|
||||||
* Automatically migrates v1 inventory to v2 format if needed.
|
* Automatically migrates v1 inventory to v2 format if needed.
|
||||||
|
|||||||
+27
-1
@@ -34,7 +34,7 @@ export let extensionSettings = {
|
|||||||
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
|
enableDeceptionSystem: false, // Enable deception tracking with <lie> tags
|
||||||
customDeceptionPrompt: '', // Custom deception prompt text (empty = use default)
|
customDeceptionPrompt: '', // Custom deception prompt text (empty = use default)
|
||||||
enableOmniscienceFilter: false, // Enable omniscience filter with <filter> tags
|
enableOmniscienceFilter: false, // Enable omniscience filter with <ofilter> tags
|
||||||
customOmnisciencePrompt: '', // Custom omniscience filter prompt text (empty = use default)
|
customOmnisciencePrompt: '', // Custom omniscience filter prompt text (empty = use default)
|
||||||
enableCYOA: false, // Enable "Choose Your Own Adventure" formatting with action choices
|
enableCYOA: false, // Enable "Choose Your Own Adventure" formatting with action choices
|
||||||
customCYOAPrompt: '', // Custom CYOA prompt text (empty = use default)
|
customCYOAPrompt: '', // Custom CYOA prompt text (empty = use default)
|
||||||
@@ -403,6 +403,32 @@ export let isPlotProgression = false;
|
|||||||
*/
|
*/
|
||||||
export let isAwaitingNewMessage = false;
|
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")
|
||||||
*/
|
*/
|
||||||
|
|||||||
+184
-1
@@ -1,8 +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.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",
|
||||||
@@ -81,6 +83,12 @@
|
|||||||
"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:",
|
||||||
@@ -176,6 +184,7 @@
|
|||||||
"template.mainPanel.coloredDialogues": "Colored Dialogues",
|
"template.mainPanel.coloredDialogues": "Colored Dialogues",
|
||||||
"template.mainPanel.deceptionSystem": "Deception System",
|
"template.mainPanel.deceptionSystem": "Deception System",
|
||||||
"template.mainPanel.omniscienceFilter": "Omniscience Filter",
|
"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",
|
||||||
@@ -194,6 +203,12 @@
|
|||||||
"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",
|
||||||
@@ -226,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",
|
||||||
@@ -238,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",
|
||||||
@@ -253,6 +277,7 @@
|
|||||||
"thoughts.clickToEdit": "Click to edit",
|
"thoughts.clickToEdit": "Click to edit",
|
||||||
"thoughts.clickToUpload": "Click to upload avatar",
|
"thoughts.clickToUpload": "Click to upload avatar",
|
||||||
"thoughts.removeCharacter": "Remove character",
|
"thoughts.removeCharacter": "Remove character",
|
||||||
|
"thoughts.empty": "No character data generated yet",
|
||||||
"userStats.level": "LVL",
|
"userStats.level": "LVL",
|
||||||
"userStats.clickToEditLevel": "Click to edit level",
|
"userStats.clickToEditLevel": "Click to edit level",
|
||||||
"userStats.statsLocked": "Locked - AI cannot change stats",
|
"userStats.statsLocked": "Locked - AI cannot change stats",
|
||||||
@@ -265,6 +290,7 @@
|
|||||||
"userStats.skillsLocked": "Locked - AI cannot change skills",
|
"userStats.skillsLocked": "Locked - AI cannot change skills",
|
||||||
"userStats.skillsUnlocked": "Unlocked - AI can change skills",
|
"userStats.skillsUnlocked": "Unlocked - AI can change skills",
|
||||||
"userStats.clickToEditSkills": "Click to edit skills",
|
"userStats.clickToEditSkills": "Click to edit skills",
|
||||||
|
"userStats.empty": "No statuses generated yet",
|
||||||
"infoBox.clickToEdit": "Click to edit",
|
"infoBox.clickToEdit": "Click to edit",
|
||||||
"infoBox.locked": "Locked - AI cannot change this",
|
"infoBox.locked": "Locked - AI cannot change this",
|
||||||
"infoBox.unlocked": "Unlocked - AI can change this",
|
"infoBox.unlocked": "Unlocked - AI can change this",
|
||||||
@@ -280,5 +306,162 @@
|
|||||||
"stats.con": "CON",
|
"stats.con": "CON",
|
||||||
"stats.int": "INT",
|
"stats.int": "INT",
|
||||||
"stats.wis": "WIS",
|
"stats.wis": "WIS",
|
||||||
"stats.cha": "CHA"
|
"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",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"settings.language.label": "Langue",
|
"settings.language.label": "Langue",
|
||||||
"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.ru": "Русский",
|
||||||
"settings.language.option.fr": "Français",
|
"settings.language.option.fr": "Français",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"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.ru": "Русский",
|
||||||
"settings.extensionEnabled": "Включить RPG Companion",
|
"settings.extensionEnabled": "Включить RPG Companion",
|
||||||
|
|||||||
@@ -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,485 @@
|
|||||||
|
{
|
||||||
|
"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.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.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,8 +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.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 設定",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ 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, DEFAULT_DECEPTION_PROMPT, DEFAULT_CYOA_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>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import {
|
|||||||
lastActionWasSwipe,
|
lastActionWasSwipe,
|
||||||
setIsGenerating,
|
setIsGenerating,
|
||||||
setLastActionWasSwipe,
|
setLastActionWasSwipe,
|
||||||
$musicPlayerContainer
|
$musicPlayerContainer,
|
||||||
|
getSeparateGenerationId
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveChatData, setMessageSwipeTrackerData } from '../../core/persistence.js';
|
import { saveChatData, setMessageSwipeTrackerData } from '../../core/persistence.js';
|
||||||
import {
|
import {
|
||||||
@@ -218,7 +219,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;
|
||||||
@@ -262,13 +263,21 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
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
|
// Track suppression state for event handler
|
||||||
let currentSuppressionState = false;
|
let currentSuppressionState = false;
|
||||||
@@ -622,25 +623,15 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
const shouldCommit = isUserMessage && !lastActionWasSwipe && currentChatLength !== lastCommittedChatLength;
|
const shouldCommit = isUserMessage && !lastActionWasSwipe && currentChatLength !== lastCommittedChatLength;
|
||||||
|
|
||||||
if (shouldCommit) {
|
if (shouldCommit) {
|
||||||
// console.log('[RPG Companion] 📝 TOGETHER MODE COMMIT: User sent message - committing data from BEFORE user message');
|
// console.log('[RPG Companion] 📝 TOGETHER MODE COMMIT: User sent message - committing from N-1 assistant message');
|
||||||
// console.log('[RPG Companion] Chat length:', currentChatLength, 'Last committed:', lastCommittedChatLength);
|
// 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)
|
// Commit from the prior assistant message's swipe store (N-1 rule).
|
||||||
committedTrackerData.userStats = lastGeneratedData.userStats;
|
// currentChatLength - 1 is the new AI placeholder; the function walks backward
|
||||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
// past it and the user message to find the previous AI message's tracker state.
|
||||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
commitTrackerDataFromPriorMessage(currentChatLength - 1);
|
||||||
|
|
||||||
// Track chat length to prevent duplicate commits
|
// Track chat length to prevent duplicate commits from streaming
|
||||||
lastCommittedChatLength = currentChatLength;
|
lastCommittedChatLength = currentChatLength;
|
||||||
|
|
||||||
// console.log('[RPG Companion] AFTER: committedTrackerData =', {
|
// console.log('[RPG Companion] AFTER: committedTrackerData =', {
|
||||||
@@ -668,38 +659,14 @@ export async function onGenerationStarted(type, data, dryRun) {
|
|||||||
// console.log('[RPG Companion DEBUG] Before generating:', lastGeneratedData.characterThoughts, ' , committed - ', committedTrackerData.characterThoughts);
|
// console.log('[RPG Companion DEBUG] Before generating:', lastGeneratedData.characterThoughts, ' , committed - ', committedTrackerData.characterThoughts);
|
||||||
if ((extensionSettings.generationMode === 'separate' || extensionSettings.generationMode === 'external') && !isGenerating) {
|
if ((extensionSettings.generationMode === 'separate' || extensionSettings.generationMode === 'external') && !isGenerating) {
|
||||||
if (!lastActionWasSwipe) {
|
if (!lastActionWasSwipe) {
|
||||||
// User sent a new message - commit lastGeneratedData before generation
|
// User sent a new message - commit from the prior assistant message's swipe store
|
||||||
// console.log('[RPG Companion] 📝 COMMIT: New message - committing lastGeneratedData');
|
// (N-1 rule) rather than lastGeneratedData, which may reflect a sibling swipe's
|
||||||
// console.log('[RPG Companion] BEFORE commit - committedTrackerData:', {
|
// outcome and would poison the context for the new generation.
|
||||||
// userStats: committedTrackerData.userStats ? 'exists' : 'null',
|
// currentChatLength - 1 is the new AI placeholder; search starts before it.
|
||||||
// infoBox: committedTrackerData.infoBox ? 'exists' : 'null',
|
commitTrackerDataFromPriorMessage(currentChatLength - 1);
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
// If lastActionWasSwipe, context was already committed by commitTrackerDataFromPriorMessage
|
||||||
|
// in onMessageSwiped before generation started.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the committed tracker data as source for generation
|
// Use the committed tracker data as source for generation
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { i18n } from '../../core/i18n.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, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -251,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;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function toFieldKey(name) {
|
|||||||
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
|
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
|
||||||
return baseName
|
return baseName
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9]+/g, '_')
|
.replace(/[^\p{L}\p{N}]+/gu, '_')
|
||||||
.replace(/^_+|_+$/g, '');
|
.replace(/^_+|_+$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,8 +229,10 @@ 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;
|
||||||
|
for (let idx = 0; idx < extractedObjects.length; idx++) {
|
||||||
|
const parsed = repairJSON(extractedObjects[idx]);
|
||||||
if (parsed && (parsed.userStats || parsed.infoBox || parsed.characters)) {
|
if (parsed && (parsed.userStats || parsed.infoBox || parsed.characters)) {
|
||||||
// console.log('[RPG Parser] ✓ Detected unified JSON structure (v3.1 format)');
|
// console.log('[RPG Parser] ✓ Detected unified JSON structure (v3.1 format)');
|
||||||
|
|
||||||
@@ -247,13 +249,17 @@ export function parseResponse(responseText) {
|
|||||||
// 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;
|
||||||
|
break; // Found unified structure, stop searching
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundUnified) {
|
||||||
// console.log('[RPG Parser] ✓ Returning unified JSON parse results');
|
// console.log('[RPG Parser] ✓ Returning unified JSON parse results');
|
||||||
debugLog('[RPG Parser] Returning unified JSON parse results');
|
|
||||||
return result;
|
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++) {
|
||||||
@@ -277,6 +283,21 @@ 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 (parsed.userStats || parsed.infoBox || parsed.characters) {
|
||||||
|
if (parsed.userStats) {
|
||||||
|
result.userStats = JSON.stringify(parsed.userStats);
|
||||||
|
}
|
||||||
|
if (parsed.infoBox) {
|
||||||
|
result.infoBox = JSON.stringify(parsed.infoBox);
|
||||||
|
}
|
||||||
|
if (parsed.characters) {
|
||||||
|
result.characterThoughts = JSON.stringify(parsed.characters);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ export const DEFAULT_DECEPTION_PROMPT = `When a character is lying or deceiving,
|
|||||||
* Default Omniscience Filter prompt text
|
* Default Omniscience Filter prompt text
|
||||||
* This instructs the AI to separate information the player character cannot perceive
|
* 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 <filter> tag using this exact format:
|
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:
|
||||||
<filter 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']"/>
|
<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: <filter 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.`;
|
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.`;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -20,9 +20,21 @@ import {
|
|||||||
setIsAwaitingNewMessage,
|
setIsAwaitingNewMessage,
|
||||||
updateLastGeneratedData,
|
updateLastGeneratedData,
|
||||||
updateCommittedTrackerData,
|
updateCommittedTrackerData,
|
||||||
$musicPlayerContainer
|
$musicPlayerContainer,
|
||||||
|
incrementSeparateGenerationId
|
||||||
} from '../../core/state.js';
|
} from '../../core/state.js';
|
||||||
import { saveChatData, loadChatData, autoSwitchPresetForEntity, getMessageSwipeTrackerData, getCurrentMessageSwipeTrackerData, restoreLatestTrackerStateFromChat, setMessageSwipeTrackerData } from '../../core/persistence.js';
|
import {
|
||||||
|
saveChatData,
|
||||||
|
loadChatData,
|
||||||
|
autoSwitchPresetForEntity,
|
||||||
|
getMessageSwipeTrackerData,
|
||||||
|
getCurrentMessageSwipeTrackerData,
|
||||||
|
restoreLatestTrackerStateFromChat,
|
||||||
|
setMessageSwipeTrackerData,
|
||||||
|
getSwipeData,
|
||||||
|
commitTrackerDataFromPriorMessage,
|
||||||
|
inheritSwipeDataFromPriorMessage
|
||||||
|
} from '../../core/persistence.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
|
|
||||||
// Generation & Parsing
|
// Generation & Parsing
|
||||||
@@ -53,6 +65,45 @@ import { restoreCheckpointOnLoad } from '../features/chapterCheckpoint.js';
|
|||||||
|
|
||||||
let chatStateRehydrateRunId = 0;
|
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
|
||||||
@@ -67,22 +118,28 @@ 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 = message.extra.rpg_companion_swipes[swipeId];
|
const swipeData = getSwipeData(message, 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;
|
||||||
}
|
}
|
||||||
@@ -351,17 +408,6 @@ export function onMessageSent() {
|
|||||||
// Note: FAB spinning is NOT shown for together mode since no extra API request is made
|
// 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
|
// The RPG data comes embedded in the main response
|
||||||
// FAB spinning is handled by apiClient.js for separate/external modes when updateRPGData() is called
|
// FAB spinning is handled by apiClient.js for separate/external modes when updateRPGData() is called
|
||||||
|
|
||||||
// For separate mode with auto-update disabled, commit displayed tracker
|
|
||||||
if (extensionSettings.generationMode === 'separate' && !extensionSettings.autoUpdate) {
|
|
||||||
if (lastGeneratedData.userStats || lastGeneratedData.infoBox || lastGeneratedData.characterThoughts) {
|
|
||||||
committedTrackerData.userStats = lastGeneratedData.userStats;
|
|
||||||
committedTrackerData.infoBox = lastGeneratedData.infoBox;
|
|
||||||
committedTrackerData.characterThoughts = lastGeneratedData.characterThoughts;
|
|
||||||
|
|
||||||
// console.log('[RPG Companion] 💾 SEPARATE MODE: Committed displayed tracker (auto-update disabled)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -499,13 +545,26 @@ 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)
|
||||||
// Only trigger if this is a newly generated message, not loading chat history
|
// Only trigger if this is a newly generated message, not loading chat history
|
||||||
if (extensionSettings.autoUpdate && isAwaitingNewMessage) {
|
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
|
// Update FAB widgets and strip widgets after separate/external mode update completes
|
||||||
setFabLoadingState(false);
|
setFabLoadingState(false);
|
||||||
updateFabWidgets();
|
updateFabWidgets();
|
||||||
@@ -562,6 +621,22 @@ export function onCharacterChanged() {
|
|||||||
// Load chat-specific data when switching chats
|
// Load chat-specific data when switching chats
|
||||||
loadChatData();
|
loadChatData();
|
||||||
|
|
||||||
|
// 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.
|
||||||
@@ -609,6 +684,9 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
// This is a NEW swipe that will trigger generation
|
// This is a NEW swipe that will trigger generation
|
||||||
setLastActionWasSwipe(true);
|
setLastActionWasSwipe(true);
|
||||||
setIsAwaitingNewMessage(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
|
||||||
@@ -621,11 +699,10 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
|
|
||||||
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
|
// console.log('[RPG Companion] Loading data for swipe', currentSwipeId);
|
||||||
|
|
||||||
// IMPORTANT: onMessageSwiped is for DISPLAY only!
|
// 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 (swipeData) {
|
if (swipeData) {
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -637,13 +714,12 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
lastGeneratedData.characterThoughts = swipeData.characterThoughts || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DON'T parse user stats when loading swipe data
|
// Sync extensionSettings.userStats so stat bars reflect this swipe
|
||||||
// This would overwrite manually edited fields (like Conditions) with old swipe data
|
if (swipeData.userStats) {
|
||||||
// The lastGeneratedData is loaded for display purposes only
|
parseUserStats(swipeData.userStats);
|
||||||
// parseUserStats() updates extensionSettings.userStats which should only be modified
|
}
|
||||||
// by new generations or manual edits, not by swipe navigation
|
|
||||||
|
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
@@ -656,6 +732,10 @@ export function onMessageSwiped(messageIndex) {
|
|||||||
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();
|
||||||
}
|
}
|
||||||
@@ -665,10 +745,102 @@ export function onMessageDeleted() {
|
|||||||
return;
|
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();
|
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();
|
saveChatData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event handler for when a message is deleted.
|
||||||
|
* Re-syncs lastGeneratedData, committedTrackerData, and all UI panels to the
|
||||||
|
* new last assistant message's active swipe — or clears everything if no
|
||||||
|
* assistant messages remain.
|
||||||
|
*/
|
||||||
|
/* Removed during merge resolution.
|
||||||
|
return;
|
||||||
|
|
||||||
|
// console.log('[RPG Companion] 🗑️ EVENT: onMessageDeleted');
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Walk backward to find the new last assistant message.
|
||||||
|
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) {
|
||||||
|
// No assistant messages remain — clear all state.
|
||||||
|
lastGeneratedData.userStats = null;
|
||||||
|
lastGeneratedData.infoBox = null;
|
||||||
|
lastGeneratedData.characterThoughts = null;
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
// console.log('[RPG Companion] 🗑️ No assistant messages remain — cleared all tracker state.');
|
||||||
|
} else {
|
||||||
|
// Restore display state from the new tail message's active swipe.
|
||||||
|
// If the message has no swipe data yet, null the fields so we
|
||||||
|
// don't show stale data from the deleted message.
|
||||||
|
const hadSwipeData = syncLastGeneratedDataFromSwipeStore(currentChat);
|
||||||
|
if (!hadSwipeData) {
|
||||||
|
lastGeneratedData.userStats = null;
|
||||||
|
lastGeneratedData.infoBox = null;
|
||||||
|
lastGeneratedData.characterThoughts = null;
|
||||||
|
committedTrackerData.userStats = null;
|
||||||
|
committedTrackerData.infoBox = null;
|
||||||
|
committedTrackerData.characterThoughts = null;
|
||||||
|
// console.log('[RPG Companion] 🗑️ No swipe data for last assistant message — cleared display state.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit context from the message *before* the new tail assistant message,
|
||||||
|
// so any subsequent generation uses the correct N-1 world state.
|
||||||
|
commitTrackerDataFromPriorMessage(lastAssistantIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-render all panels.
|
||||||
|
renderUserStats();
|
||||||
|
renderInfoBox();
|
||||||
|
renderThoughts();
|
||||||
|
renderInventory();
|
||||||
|
renderQuests();
|
||||||
|
renderMusicPlayer($musicPlayerContainer[0]);
|
||||||
|
|
||||||
|
// Update widget strips.
|
||||||
|
updateFabWidgets();
|
||||||
|
updateStripWidgets();
|
||||||
|
|
||||||
|
// Persist updated state.
|
||||||
|
saveChatData();
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the persona avatar image when user switches personas
|
* Update the persona avatar image when user switches personas
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -363,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="${i18n.getTranslation('infoBox.clickToEdit')}">${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="${i18n.getTranslation('infoBox.clickToEdit')}"><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="${i18n.getTranslation('infoBox.clickToEdit')}">${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>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@@ -373,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 || i18n.getTranslation('infoBox.weatherFallback');
|
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="${i18n.getTranslation('userStats.clickToEditEmoji')}">${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="${i18n.getTranslation('infoBox.clickToEdit')}">${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>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@@ -429,7 +429,7 @@ 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="${i18n.getTranslation('infoBox.clickToEdit')}">${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>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@@ -464,9 +464,9 @@ export function renderInfoBox() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-time-range">
|
<div class="rpg-time-range">
|
||||||
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="timeStart" title="${i18n.getTranslation('infoBox.clickToEdit')}">${timeStartDisplay}</div>
|
<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>
|
<span class="rpg-time-separator">→</span>
|
||||||
<div class="rpg-time-value rpg-editable" contenteditable="true" data-field="timeEnd" title="${i18n.getTranslation('infoBox.clickToEdit')}">${timeEndDisplay}</div>
|
<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>
|
</div>
|
||||||
`);
|
`);
|
||||||
@@ -481,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 || i18n.getTranslation('infoBox.locationFallback');
|
const locationDisplay = data.location || i18n.getTranslation('infoBox.locationFallback') || 'Unknown location';
|
||||||
const locationLockIconHtml = getLockIconHtml('infoBox', 'location');
|
const locationLockIconHtml = getLockIconHtml('infoBox', 'location');
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
@@ -491,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="${i18n.getTranslation('infoBox.clickToEdit')}">${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>
|
||||||
`;
|
`;
|
||||||
@@ -541,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">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -550,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="${i18n.getTranslation('infoBox.clickToEdit')}">${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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -560,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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -652,7 +652,7 @@ export function renderInfoBox() {
|
|||||||
|
|
||||||
// Update icon
|
// Update icon
|
||||||
$lockIcon.text(newLockState ? '🔒' : '🔓');
|
$lockIcon.text(newLockState ? '🔒' : '🔓');
|
||||||
$lockIcon.attr('title', newLockState ? i18n.getTranslation('infoBox.locked') : i18n.getTranslation('infoBox.unlocked'));
|
$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
|
||||||
|
|||||||
@@ -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,7 +82,7 @@ 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
|
||||||
@@ -85,10 +91,10 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
|
|||||||
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 {
|
||||||
@@ -98,8 +104,8 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
|
|||||||
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,7 +164,7 @@ 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
|
||||||
@@ -167,10 +173,10 @@ export function renderClothingView(clothingItems, viewMode = 'list') {
|
|||||||
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 {
|
||||||
@@ -180,8 +186,8 @@ export function renderClothingView(clothingItems, viewMode = 'list') {
|
|||||||
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,7 +292,7 @@ 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
|
||||||
@@ -295,10 +301,10 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
|||||||
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 {
|
||||||
@@ -308,8 +314,8 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
|
|||||||
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,7 +394,7 @@ 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
|
||||||
@@ -397,10 +403,10 @@ export function renderAssetsView(assets, viewMode = 'list') {
|
|||||||
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 {
|
||||||
@@ -410,8 +416,8 @@ export function renderAssetsView(assets, viewMode = 'list') {
|
|||||||
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>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { extensionSettings, $questsContainer, committedTrackerData, lastGeneratedData } from '../../core/state.js';
|
import { extensionSettings, $questsContainer, committedTrackerData, lastGeneratedData } from '../../core/state.js';
|
||||||
import { saveSettings, saveChatData } 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
|
* Syncs the current extensionSettings.quests to committedTrackerData.userStats
|
||||||
@@ -44,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>`;
|
||||||
}
|
}
|
||||||
@@ -66,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>
|
||||||
`;
|
`;
|
||||||
@@ -86,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">
|
||||||
@@ -101,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>
|
||||||
@@ -112,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>
|
||||||
`;
|
`;
|
||||||
@@ -150,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>
|
||||||
@@ -172,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>
|
||||||
@@ -194,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>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function getLockIconHtml(tracker, path) {
|
|||||||
|
|
||||||
const isLocked = isItemLocked(tracker, path);
|
const isLocked = isItemLocked(tracker, path);
|
||||||
const lockIcon = isLocked ? '🔒' : '🔓';
|
const lockIcon = isLocked ? '🔒' : '🔓';
|
||||||
const lockTitle = isLocked ? i18n.getTranslation('thoughts.locked') : i18n.getTranslation('thoughts.unlocked');
|
const lockTitle = isLocked ? i18n.getTranslation('thoughts.locked') || 'Locked' : i18n.getTranslation('thoughts.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>`;
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ export function renderThoughts({ preserveScroll = false, useCommittedFallback =
|
|||||||
// Don't render if no data exists (e.g., after cache clear)
|
// Don't render if no data exists (e.g., after cache clear)
|
||||||
const thoughtsData = getPresentCharactersTrackerData({ useCommittedFallback });
|
const thoughtsData = getPresentCharactersTrackerData({ useCommittedFallback });
|
||||||
if (!thoughtsData) {
|
if (!thoughtsData) {
|
||||||
$thoughtsContainer.html('<div class="rpg-inventory-empty">No character data generated yet</div>');
|
$thoughtsContainer.html('<div class="rpg-inventory-empty">' + (i18n.getTranslation('thoughts.empty') || 'No character data generated yet') + '</div>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,14 +381,14 @@ export function renderThoughts({ preserveScroll = false, useCommittedFallback =
|
|||||||
html += `
|
html += `
|
||||||
<div class="rpg-character-card" data-character-name="${char.name}">
|
<div class="rpg-character-card" data-character-name="${char.name}">
|
||||||
<div class="rpg-character-header-row">
|
<div class="rpg-character-header-row">
|
||||||
<div class="rpg-character-avatar rpg-avatar-upload" data-character="${char.name}" title="${i18n.getTranslation('thoughts.clickToUpload')}">
|
<div class="rpg-character-avatar rpg-avatar-upload" data-character="${char.name}" title="${i18n.getTranslation('thoughts.clickToUpload') || 'Click to upload avatar'}">
|
||||||
<img src="${characterPortrait}" alt="${char.name}" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
<img src="${characterPortrait}" alt="${char.name}" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
||||||
${hasRelationshipEnabled ? `<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${relationshipFieldName}" title="${i18n.getTranslation('thoughts.clickToEdit')} (emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipBadge}</div>` : ''}
|
${hasRelationshipEnabled ? `<div class="rpg-relationship-badge rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${relationshipFieldName}" title="${i18n.getTranslation('thoughts.clickToEdit') || 'Click to edit'} (emoji: ⚔️ ⚖️ ⭐ ❤️)">${relationshipBadge}</div>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-character-header">
|
<div class="rpg-character-header">
|
||||||
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="${i18n.getTranslation('thoughts.clickToEdit')}">${char.emoji}</span>
|
<span class="rpg-character-emoji rpg-editable" contenteditable="true" data-character="${char.name}" data-field="emoji" title="${i18n.getTranslation('thoughts.clickToEdit') || 'Click to edit'}">${char.emoji}</span>
|
||||||
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="${i18n.getTranslation('thoughts.clickToEdit')}">${char.name}</span>
|
<span class="rpg-character-name rpg-editable" contenteditable="true" data-character="${char.name}" data-field="name" title="${i18n.getTranslation('thoughts.clickToEdit') || 'Click to edit'}">${char.name}</span>
|
||||||
<button class="rpg-character-remove" data-character="${char.name}" title="${i18n.getTranslation('thoughts.removeCharacter')}">×</button>
|
<button class="rpg-character-remove" data-character="${char.name}" title="${i18n.getTranslation('thoughts.removeCharacter') || 'Remove character'}">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-character-content">
|
<div class="rpg-character-content">
|
||||||
@@ -411,12 +411,12 @@ export function renderThoughts({ preserveScroll = false, useCommittedFallback =
|
|||||||
html += `
|
html += `
|
||||||
<div class="rpg-character-field rpg-character-${fieldId}" style="position: relative;">
|
<div class="rpg-character-field rpg-character-${fieldId}" style="position: relative;">
|
||||||
${lockIconHtml}
|
${lockIconHtml}
|
||||||
<span class="rpg-editable${emptyClass}" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="${i18n.getTranslation('thoughts.clickToEdit')}" ${placeholder}>${fieldValue}</span>
|
<span class="rpg-editable${emptyClass}" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="${i18n.getTranslation('thoughts.clickToEdit') || 'Click to edit'}" ${placeholder}>${fieldValue}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
html += `
|
html += `
|
||||||
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable${emptyClass}" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="${i18n.getTranslation('thoughts.clickToEdit')}" ${placeholder}>${fieldValue}</div>
|
<div class="rpg-character-field rpg-character-${fieldId} rpg-editable${emptyClass}" contenteditable="true" data-character="${char.name}" data-field="${field.name}" title="${i18n.getTranslation('thoughts.clickToEdit') || 'Click to edit'}" ${placeholder}>${fieldValue}</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -442,7 +442,7 @@ export function renderThoughts({ preserveScroll = false, useCommittedFallback =
|
|||||||
);
|
);
|
||||||
html += `
|
html += `
|
||||||
<div class="rpg-character-stat">
|
<div class="rpg-character-stat">
|
||||||
<span class="rpg-stat-name">${stat.name}: </span><span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${stat.name}" style="color: ${statColor}" title="${i18n.getTranslation('thoughts.clickToEdit')}">${statValue}%</span>
|
<span class="rpg-stat-name">${stat.name}: </span><span class="rpg-editable" contenteditable="true" data-character="${char.name}" data-field="${stat.name}" style="color: ${statColor}" title="${i18n.getTranslation('thoughts.clickToEdit') || 'Click to edit'}">${statValue}%</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -468,8 +468,8 @@ export function renderThoughts({ preserveScroll = false, useCommittedFallback =
|
|||||||
// Add "Add Character" button if data exists (inside rpg-thoughts-content)
|
// Add "Add Character" button if data exists (inside rpg-thoughts-content)
|
||||||
if (presentCharacters.length > 0) {
|
if (presentCharacters.length > 0) {
|
||||||
html += `
|
html += `
|
||||||
<button class="rpg-add-character-btn" title="${i18n.getTranslation('thoughts.addCharacter')}">
|
<button class="rpg-add-character-btn" title="${i18n.getTranslation('thoughts.addCharacter') || 'Add character'}">
|
||||||
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('thoughts.addCharacter')}
|
<i class="fa-solid fa-plus"></i> ${i18n.getTranslation('thoughts.addCharacter') || 'Add character'}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -510,7 +510,9 @@ export function renderThoughts({ preserveScroll = false, useCommittedFallback =
|
|||||||
|
|
||||||
// Update icon
|
// Update icon
|
||||||
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
||||||
const newTitle = !currentlyLocked ? 'Locked' : 'Unlocked';
|
const newTitle = !currentlyLocked
|
||||||
|
? (i18n.getTranslation('thoughts.locked') || 'Locked')
|
||||||
|
: (i18n.getTranslation('thoughts.unlocked') || 'Unlocked');
|
||||||
$icon.text(newIcon);
|
$icon.text(newIcon);
|
||||||
$icon.attr('title', newTitle);
|
$icon.attr('title', newTitle);
|
||||||
|
|
||||||
@@ -1307,7 +1309,7 @@ function renderThoughtsSidebarOnly() {
|
|||||||
// Copy the rendering logic from renderThoughts but skip the updateChatThoughts call
|
// Copy the rendering logic from renderThoughts but skip the updateChatThoughts call
|
||||||
const thoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts;
|
const thoughtsData = lastGeneratedData.characterThoughts || committedTrackerData.characterThoughts;
|
||||||
if (!thoughtsData) {
|
if (!thoughtsData) {
|
||||||
$thoughtsContainer.html('<div class="rpg-inventory-empty">No character data generated yet</div>');
|
$thoughtsContainer.html('<div class="rpg-inventory-empty">' + (i18n.getTranslation('thoughts.empty') || 'No character data generated yet') + '</div>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2436,7 +2438,9 @@ export function createThoughtPanel($message, thoughtsArray) {
|
|||||||
|
|
||||||
// Update icon
|
// Update icon
|
||||||
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
||||||
const newTitle = !currentlyLocked ? 'Locked' : 'Unlocked';
|
const newTitle = !currentlyLocked
|
||||||
|
? (i18n.getTranslation('thoughts.locked') || 'Locked')
|
||||||
|
: (i18n.getTranslation('thoughts.unlocked') || 'Unlocked');
|
||||||
$icon.text(newIcon);
|
$icon.text(newIcon);
|
||||||
$icon.attr('title', newTitle);
|
$icon.attr('title', newTitle);
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function toFieldKey(name) {
|
|||||||
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
|
const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim();
|
||||||
return baseName
|
return baseName
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9]+/g, '_')
|
.replace(/[^\p{L}\p{N}]+/gu, '_')
|
||||||
.replace(/^_+|_+$/g, '');
|
.replace(/^_+|_+$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +213,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@ export function renderUserStats() {
|
|||||||
// 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 ? i18n.getTranslation('userStats.statsLocked') : i18n.getTranslation('userStats.statsUnlocked');
|
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">';
|
||||||
@@ -287,8 +287,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">${i18n.getTranslation('userStats.level')}</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="${i18n.getTranslation('userStats.clickToEditLevel')}">${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>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -321,11 +321,11 @@ export function renderUserStats() {
|
|||||||
|
|
||||||
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="${i18n.getTranslation('userStats.clickToEditStatName')}">${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 - percentage}%"></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}" data-max="${maxValue}" data-mode="${displayMode}" title="${i18n.getTranslation('userStats.clickToEditStatValue')}">${displayValue}</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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -335,7 +335,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 ? i18n.getTranslation('userStats.moodLocked') : i18n.getTranslation('userStats.moodUnlocked');
|
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) {
|
||||||
@@ -343,7 +343,7 @@ 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="${i18n.getTranslation('userStats.clickToEditEmoji')}">${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
|
||||||
@@ -358,7 +358,7 @@ export function renderUserStats() {
|
|||||||
// Strip brackets if present (from JSON array format)
|
// Strip brackets if present (from JSON array format)
|
||||||
fieldValue = fieldValue.replace(/^\[|\]$/g, '').trim();
|
fieldValue = fieldValue.replace(/^\[|\]$/g, '').trim();
|
||||||
}
|
}
|
||||||
html += `<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="${fieldKey}" title="Click to edit ${fieldName}">${fieldValue}</div>`;
|
html += `<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="${fieldKey}" title="${i18n.getTranslation('userStats.clickToEdit') || 'Click to edit'} ${fieldName}">${fieldValue}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +369,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 ? i18n.getTranslation('userStats.skillsLocked') : i18n.getTranslation('userStats.skillsUnlocked');
|
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"}]
|
||||||
@@ -386,7 +386,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="${i18n.getTranslation('userStats.clickToEditSkills')}">${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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -595,7 +595,7 @@ export function renderUserStats() {
|
|||||||
|
|
||||||
// Update icon
|
// Update icon
|
||||||
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
const newIcon = !currentlyLocked ? '🔒' : '🔓';
|
||||||
const newTitle = !currentlyLocked ? i18n.getTranslation('infoBox.locked') : i18n.getTranslation('infoBox.unlocked');
|
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);
|
||||||
|
|
||||||
|
|||||||
@@ -71,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');
|
||||||
@@ -88,7 +88,7 @@ export class EncounterModal {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,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;
|
||||||
}
|
}
|
||||||
@@ -142,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>
|
||||||
@@ -303,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>
|
||||||
@@ -316,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 -->
|
||||||
@@ -331,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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -368,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>
|
||||||
@@ -381,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>
|
||||||
@@ -389,10 +389,10 @@ 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>
|
||||||
@@ -420,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==';
|
||||||
@@ -434,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">
|
||||||
@@ -481,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('')}
|
||||||
@@ -561,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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -594,7 +594,7 @@ 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) {
|
||||||
@@ -609,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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -618,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>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -661,7 +661,7 @@ export class EncounterModal {
|
|||||||
renderPlayerControls(party, playerActions = null) {
|
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
|
// Use playerActions if provided, otherwise fall back to player data
|
||||||
@@ -670,11 +670,11 @@ export class EncounterModal {
|
|||||||
|
|
||||||
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>
|
||||||
${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;
|
||||||
@@ -686,7 +686,7 @@ 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>
|
||||||
`;
|
`;
|
||||||
@@ -695,7 +695,7 @@ export class EncounterModal {
|
|||||||
|
|
||||||
${items && 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>
|
||||||
${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}
|
||||||
@@ -706,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>
|
||||||
@@ -746,15 +746,15 @@ export class EncounterModal {
|
|||||||
if (!target) return;
|
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') {
|
||||||
const target = await this.showTargetSelection('single-target', currentEncounter.combatStats);
|
const target = await this.showTargetSelection('single-target', currentEncounter.combatStats);
|
||||||
if (!target) return;
|
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);
|
||||||
@@ -809,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);
|
||||||
@@ -823,7 +823,7 @@ export class EncounterModal {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -831,7 +831,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -899,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 => {
|
||||||
@@ -959,7 +959,7 @@ export class EncounterModal {
|
|||||||
|
|
||||||
if (player && player.hp <= 0) {
|
if (player && player.hp <= 0) {
|
||||||
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) {
|
} else if (currentEncounter.playerActions && controlsContainer) {
|
||||||
// Check if actions have changed by comparing with previous state
|
// Check if actions have changed by comparing with previous state
|
||||||
@@ -1205,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>
|
||||||
`;
|
`;
|
||||||
@@ -1234,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>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -1251,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>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -1321,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>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
@@ -38,10 +38,23 @@ export function updateMobileTabLabels() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 'quests':
|
||||||
|
fallback = 'Quests';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
const translation = i18n.getTranslation(translationKey) || fallback;
|
||||||
|
$tab.find('span').text(translation);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -609,19 +622,19 @@ export function setupMobileTabs() {
|
|||||||
|
|
||||||
// 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 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>');
|
||||||
|
|||||||
@@ -324,7 +324,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { renderUserStats } from '../rendering/userStats.js';
|
|||||||
import { renderInfoBox } from '../rendering/infoBox.js';
|
import { renderInfoBox } from '../rendering/infoBox.js';
|
||||||
import { renderThoughts } from '../rendering/thoughts.js';
|
import { renderThoughts } from '../rendering/thoughts.js';
|
||||||
import { updateFabWidgets } from './mobile.js';
|
import { updateFabWidgets } from './mobile.js';
|
||||||
|
import { safeToSnake } from '../../utils/transformations.js';
|
||||||
|
|
||||||
let $editorModal = null;
|
let $editorModal = null;
|
||||||
let activeTab = 'userStats';
|
let activeTab = 'userStats';
|
||||||
@@ -38,6 +39,36 @@ let tempConfig = null; // Temporary config for cancel functionality
|
|||||||
let tempAssociation = null; // Temporary association state: { presetId: string|null, entityKey: string|null }
|
let tempAssociation = null; // Temporary association state: { presetId: string|null, entityKey: string|null }
|
||||||
let originalAssociation = null; // Original association when editor opened
|
let originalAssociation = null; // Original association when editor opened
|
||||||
|
|
||||||
|
|
||||||
|
function set_ids_names(list_with_stats, index, value) {
|
||||||
|
list_with_stats[index].name = value;
|
||||||
|
const item = list_with_stats[index];
|
||||||
|
const oldId = item?.id;
|
||||||
|
|
||||||
|
item.name = value;
|
||||||
|
const ids = list_with_stats.filter((_, i) => i !== index).map(stat => stat.id);
|
||||||
|
const snake_value = safeToSnake(value); // new id format
|
||||||
|
if (snake_value !== value && !ids.includes(snake_value)) { // check if this id already exists
|
||||||
|
item.id = snake_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newId = item.id;
|
||||||
|
// If the ID changed, migrate any stored values keyed by the old ID
|
||||||
|
if (oldId && newId && oldId !== newId) {
|
||||||
|
if (extensionSettings.userStats && Object.prototype.hasOwnProperty.call(extensionSettings.userStats, oldId)) {
|
||||||
|
extensionSettings.userStats[newId] = extensionSettings.userStats[oldId];
|
||||||
|
delete extensionSettings.userStats[oldId];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensionSettings.classicStats && Object.prototype.hasOwnProperty.call(extensionSettings.classicStats, oldId)) {
|
||||||
|
extensionSettings.classicStats[newId] = extensionSettings.classicStats[oldId];
|
||||||
|
delete extensionSettings.classicStats[oldId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list_with_stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the tracker editor modal
|
* Initialize the tracker editor modal
|
||||||
*/
|
*/
|
||||||
@@ -225,7 +256,7 @@ function updatePresetUI() {
|
|||||||
// Update the default button appearance
|
// Update the default button appearance
|
||||||
const $defaultBtn = $('#rpg-preset-default');
|
const $defaultBtn = $('#rpg-preset-default');
|
||||||
if (isDefaultPreset(activePresetId)) {
|
if (isDefaultPreset(activePresetId)) {
|
||||||
$defaultBtn.addClass('rpg-btn-active').attr('title', 'This is the default preset');
|
$defaultBtn.addClass('rpg-btn-active').attr('title', i18n.getTranslation('preset.defaultPresetDescription') || 'This is the default preset');
|
||||||
} else {
|
} else {
|
||||||
$defaultBtn.removeClass('rpg-btn-active').attr('title', 'Set as Default Preset');
|
$defaultBtn.removeClass('rpg-btn-active').attr('title', 'Set as Default Preset');
|
||||||
}
|
}
|
||||||
@@ -334,18 +365,18 @@ function resetToDefaults() {
|
|||||||
extensionSettings.trackerConfig = {
|
extensionSettings.trackerConfig = {
|
||||||
userStats: {
|
userStats: {
|
||||||
customStats: [
|
customStats: [
|
||||||
{ id: 'health', name: i18n.getTranslation('stats.health'), enabled: true, persistInHistory: false },
|
{ id: 'health', name: i18n.getTranslation('stats.health') || 'Health', enabled: true, persistInHistory: false },
|
||||||
{ id: 'satiety', name: i18n.getTranslation('stats.satiety'), enabled: true, persistInHistory: false },
|
{ id: 'satiety', name: i18n.getTranslation('stats.satiety') || 'Satiety', enabled: true, persistInHistory: false },
|
||||||
{ id: 'energy', name: i18n.getTranslation('stats.energy'), enabled: true, persistInHistory: false },
|
{ id: 'energy', name: i18n.getTranslation('stats.energy') || 'Energy', enabled: true, persistInHistory: false },
|
||||||
{ id: 'hygiene', name: i18n.getTranslation('stats.hygiene'), enabled: true, persistInHistory: false },
|
{ id: 'hygiene', name: i18n.getTranslation('stats.hygiene') || 'Hygiene', enabled: true, persistInHistory: false },
|
||||||
{ id: 'arousal', name: i18n.getTranslation('stats.arousal'), enabled: true, persistInHistory: false }
|
{ id: 'arousal', name: i18n.getTranslation('stats.arousal') || 'Arousal', enabled: true, persistInHistory: false }
|
||||||
],
|
],
|
||||||
showRPGAttributes: true,
|
showRPGAttributes: true,
|
||||||
rpgAttributes: [
|
rpgAttributes: [
|
||||||
{ id: 'str', name: i18n.getTranslation('stats.str'), enabled: true, persistInHistory: false },
|
{ id: 'str', name: i18n.getTranslation('stats.str') || 'STR', enabled: true, persistInHistory: false },
|
||||||
{ id: 'dex', name: i18n.getTranslation('stats.dex'), enabled: true, persistInHistory: false },
|
{ id: 'dex', name: i18n.getTranslation('stats.dex') || 'DEX', enabled: true, persistInHistory: false },
|
||||||
{ id: 'con', name: i18n.getTranslation('stats.con'), enabled: true, persistInHistory: false },
|
{ id: 'con', name: i18n.getTranslation('stats.con') || 'CON', enabled: true, persistInHistory: false },
|
||||||
{ id: 'int', name: i18n.getTranslation('stats.int'), enabled: true, persistInHistory: false },
|
{ id: 'int', name: i18n.getTranslation('stats.int') || 'INT', enabled: true, persistInHistory: false },
|
||||||
{ id: 'wis', name: i18n.getTranslation('stats.wis'), enabled: true, persistInHistory: false },
|
{ id: 'wis', name: i18n.getTranslation('stats.wis'), enabled: true, persistInHistory: false },
|
||||||
{ id: 'cha', name: i18n.getTranslation('stats.cha'), enabled: true, persistInHistory: false }
|
{ id: 'cha', name: i18n.getTranslation('stats.cha'), enabled: true, persistInHistory: false }
|
||||||
],
|
],
|
||||||
@@ -635,7 +666,7 @@ function showImportModeDialog(migratedConfig, suggestedName, historyPersistence
|
|||||||
</button>
|
</button>
|
||||||
<button id="rpg-import-as-new" class="rpg-btn-primary">
|
<button id="rpg-import-as-new" class="rpg-btn-primary">
|
||||||
<i class="fa-solid fa-plus"></i>
|
<i class="fa-solid fa-plus"></i>
|
||||||
Create New Preset
|
${i18n.getTranslation('preset.createNewPresetTitle') || 'Create New Preset'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="rpg-import-cancel" class="rpg-btn-cancel">Cancel</button>
|
<button id="rpg-import-cancel" class="rpg-btn-cancel">Cancel</button>
|
||||||
@@ -728,15 +759,15 @@ function renderUserStatsTab() {
|
|||||||
let html = '<div class="rpg-editor-section">';
|
let html = '<div class="rpg-editor-section">';
|
||||||
|
|
||||||
// Custom Stats section
|
// Custom Stats section
|
||||||
html += `<h4><i class="fa-solid fa-heart-pulse"></i> ${i18n.getTranslation('template.trackerEditorModal.userStatsTab.customStatsTitle')}</h4>`;
|
html += `<h4><i class="fa-solid fa-heart-pulse"></i> ${i18n.getTranslation('template.trackerEditorModal.userStatsTab.customStatsTitle') || 'Custom Stats'}</h4>`;
|
||||||
|
|
||||||
// Stats display mode toggle
|
// Stats display mode toggle
|
||||||
const statsDisplayMode = config.statsDisplayMode || 'percentage';
|
const statsDisplayMode = config.statsDisplayMode || 'percentage';
|
||||||
html += '<div class="rpg-editor-toggle-row">';
|
html += '<div class="rpg-editor-toggle-row">';
|
||||||
html += '<label>Display Mode:</label>';
|
html += '<label>' + (i18n.getTranslation('stats.displayMode') || 'Display Mode:') + '</label>';
|
||||||
html += '<div class="rpg-radio-group">';
|
html += '<div class="rpg-radio-group">';
|
||||||
html += `<label><input type="radio" name="stats-display-mode" value="percentage" ${statsDisplayMode === 'percentage' ? 'checked' : ''}> Percentage</label>`;
|
html += `<label><input type="radio" name="stats-display-mode" value="percentage" ${statsDisplayMode === 'percentage' ? 'checked' : ''}> ${i18n.getTranslation('stats.displayMode.percentage') || 'Percentage'}</label>`;
|
||||||
html += `<label><input type="radio" name="stats-display-mode" value="number" ${statsDisplayMode === 'number' ? 'checked' : ''}> Number</label>`;
|
html += `<label><input type="radio" name="stats-display-mode" value="number" ${statsDisplayMode === 'number' ? 'checked' : ''}> ${i18n.getTranslation('stats.displayMode.number') || 'Number'}</label>`;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
@@ -772,7 +803,7 @@ function renderUserStatsTab() {
|
|||||||
const showLevel = config.showLevel !== undefined ? config.showLevel : true;
|
const showLevel = config.showLevel !== undefined ? config.showLevel : true;
|
||||||
html += '<div class="rpg-editor-toggle-row">';
|
html += '<div class="rpg-editor-toggle-row">';
|
||||||
html += `<input type="checkbox" id="rpg-show-level" ${showLevel ? 'checked' : ''}>`;
|
html += `<input type="checkbox" id="rpg-show-level" ${showLevel ? 'checked' : ''}>`;
|
||||||
html += `<label for="rpg-show-level">Show Level</label>`;
|
html += `<label for="rpg-show-level">${i18n.getTranslation('stats.showLevel') || 'Show Level'}</label>`;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// Always send attributes toggle
|
// Always send attributes toggle
|
||||||
@@ -885,7 +916,9 @@ function setupUserStatsListeners() {
|
|||||||
// Rename stat
|
// Rename stat
|
||||||
$('.rpg-stat-name').off('blur').on('blur', function () {
|
$('.rpg-stat-name').off('blur').on('blur', function () {
|
||||||
const index = $(this).data('index');
|
const index = $(this).data('index');
|
||||||
extensionSettings.trackerConfig.userStats.customStats[index].name = $(this).val();
|
const value = $(this).val();
|
||||||
|
const list_with_stats = extensionSettings.trackerConfig.userStats.customStats
|
||||||
|
set_ids_names(list_with_stats, index, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Change stat max value
|
// Change stat max value
|
||||||
@@ -943,7 +976,9 @@ function setupUserStatsListeners() {
|
|||||||
// Rename attribute
|
// Rename attribute
|
||||||
$('.rpg-attr-name').off('blur').on('blur', function () {
|
$('.rpg-attr-name').off('blur').on('blur', function () {
|
||||||
const index = $(this).data('index');
|
const index = $(this).data('index');
|
||||||
extensionSettings.trackerConfig.userStats.rpgAttributes[index].name = $(this).val();
|
const value = $(this).val();
|
||||||
|
const list_with_stats = extensionSettings.trackerConfig.userStats.rpgAttributes
|
||||||
|
set_ids_names(list_with_stats, index, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Enable/disable RPG Attributes section toggle
|
// Enable/disable RPG Attributes section toggle
|
||||||
@@ -1006,8 +1041,8 @@ function renderInfoBoxTab() {
|
|||||||
html += `<input type="checkbox" id="rpg-widget-date" ${config.widgets.date.enabled ? 'checked' : ''}>`;
|
html += `<input type="checkbox" id="rpg-widget-date" ${config.widgets.date.enabled ? 'checked' : ''}>`;
|
||||||
html += `<label for="rpg-widget-date">${i18n.getTranslation('template.trackerEditorModal.infoBoxTab.dateWidget')}</label>`;
|
html += `<label for="rpg-widget-date">${i18n.getTranslation('template.trackerEditorModal.infoBoxTab.dateWidget')}</label>`;
|
||||||
html += '<select id="rpg-date-format" class="rpg-select-mini">';
|
html += '<select id="rpg-date-format" class="rpg-select-mini">';
|
||||||
html += `<option value="Weekday, Month, Year" ${config.widgets.date.format === 'Weekday, Month, Year' ? 'selected' : ''}>Weekday, Month, Year</option>`;
|
html += `<option value="Weekday, Month, Year" ${config.widgets.date.format === 'Weekday, Month, Year' ? 'selected' : ''}>${i18n.getTranslation('dateFormat.weekdayMonthYear') || 'Weekday, Month, Year'}</option>`;
|
||||||
html += `<option value="Day (Numerical), Month, Year" ${config.widgets.date.format === 'Day (Numerical), Month, Year' ? 'selected' : ''}>Day (Numerical), Month, Year</option>`;
|
html += `<option value="Day (Numerical), Month, Year" ${config.widgets.date.format === 'Day (Numerical), Month, Year' ? 'selected' : ''}>${i18n.getTranslation('dateFormat.dayNumericalMonthYear') || 'Day (Numerical), Month, Year'}</option>`;
|
||||||
html += '</select>';
|
html += '</select>';
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
@@ -1394,7 +1429,9 @@ function setupPresentCharactersListeners() {
|
|||||||
// Rename field
|
// Rename field
|
||||||
$('.rpg-field-label').off('blur').on('blur', function () {
|
$('.rpg-field-label').off('blur').on('blur', function () {
|
||||||
const index = $(this).data('index');
|
const index = $(this).data('index');
|
||||||
extensionSettings.trackerConfig.presentCharacters.customFields[index].name = $(this).val();
|
const value = $(this).val();
|
||||||
|
const list_with_stats = extensionSettings.trackerConfig.presentCharacters.customFields
|
||||||
|
set_ids_names(list_with_stats, index, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update description
|
// Update description
|
||||||
@@ -1443,7 +1480,9 @@ function setupPresentCharactersListeners() {
|
|||||||
// Rename character stat
|
// Rename character stat
|
||||||
$('.rpg-char-stat-label').off('blur').on('blur', function () {
|
$('.rpg-char-stat-label').off('blur').on('blur', function () {
|
||||||
const index = $(this).data('index');
|
const index = $(this).data('index');
|
||||||
extensionSettings.trackerConfig.presentCharacters.characterStats.customStats[index].name = $(this).val();
|
const value = $(this).val();
|
||||||
|
const list_with_stats = extensionSettings.trackerConfig.presentCharacters.characterStats.customStats
|
||||||
|
set_ids_names(list_with_stats, index, value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1467,48 +1506,48 @@ function renderHistoryPersistenceTab() {
|
|||||||
let html = '<div class="rpg-editor-section">';
|
let html = '<div class="rpg-editor-section">';
|
||||||
|
|
||||||
// Main toggle and settings
|
// Main toggle and settings
|
||||||
html += `<h4><i class="fa-solid fa-clock-rotate-left"></i> History Persistence Settings</h4>`;
|
html += `<h4><i class="fa-solid fa-clock-rotate-left"></i> ${i18n.getTranslation('historyPersistence.settingsTitle') || 'History Persistence Settings'}</h4>`;
|
||||||
html += `<p class="rpg-editor-hint">Inject selected tracker data into historical messages to help the AI maintain continuity for time-sensitive events, weather changes, and location tracking.</p>`;
|
html += `<p class="rpg-editor-hint">${i18n.getTranslation('historyPersistence.hint') || 'Inject selected tracker data into historical messages to help the AI maintain continuity for time-sensitive events, weather changes, and location tracking.'}</p>`;
|
||||||
|
|
||||||
// Enable toggle
|
// Enable toggle
|
||||||
html += '<div class="rpg-editor-toggle-row">';
|
html += '<div class="rpg-editor-toggle-row">';
|
||||||
html += `<input type="checkbox" id="rpg-history-persistence-enabled" ${historyPersistence.enabled ? 'checked' : ''}>`;
|
html += `<input type="checkbox" id="rpg-history-persistence-enabled" ${historyPersistence.enabled ? 'checked' : ''}>`;
|
||||||
html += `<label for="rpg-history-persistence-enabled">Enable History Persistence</label>`;
|
html += `<label for="rpg-history-persistence-enabled">${i18n.getTranslation('historyPersistence.enable') || 'Enable History Persistence'}</label>`;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// External API Only toggle - only show for separate/external modes
|
// External API Only toggle - only show for separate/external modes
|
||||||
if (generationMode === 'separate' || generationMode === 'external') {
|
if (generationMode === 'separate' || generationMode === 'external') {
|
||||||
html += '<div class="rpg-editor-toggle-row" style="margin-top: 8px;">';
|
html += '<div class="rpg-editor-toggle-row" style="margin-top: 8px;">';
|
||||||
html += `<input type="checkbox" id="rpg-history-send-all-enabled" ${historyPersistence.sendAllEnabledOnRefresh ? 'checked' : ''}>`;
|
html += `<input type="checkbox" id="rpg-history-send-all-enabled" ${historyPersistence.sendAllEnabledOnRefresh ? 'checked' : ''}>`;
|
||||||
html += `<label for="rpg-history-send-all-enabled">Send All Enabled Stats on Refresh</label>`;
|
html += `<label for="rpg-history-send-all-enabled">${i18n.getTranslation('historyPersistence.sendAllEnabledStats') || 'Send All Enabled Stats on Refresh'}</label>`;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
html += `<p class="rpg-editor-hint" style="margin-top: 4px; margin-left: 24px;">When enabled, Refresh RPG Info will include all enabled stats from the preset in history context, ignoring the individual selections below.</p>`;
|
html += `<p class="rpg-editor-hint" style="margin-top: 4px; margin-left: 24px;">${i18n.getTranslation('historyPersistence.sendAllEnabledStatsHint') || 'When enabled, Refresh RPG Info will include all enabled stats from the preset in history context, ignoring the individual selections below.'}</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message count
|
// Message count
|
||||||
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
||||||
html += `<label for="rpg-history-message-count">Number of messages to include (0 = all available):</label>`;
|
html += `<label for="rpg-history-message-count">${i18n.getTranslation('historyPersistence.numberOfMessages') || 'Number of messages to include (0 = all available):'}</label>`;
|
||||||
html += `<input type="number" id="rpg-history-message-count" min="0" max="50" value="${historyPersistence.messageCount}" class="rpg-input" style="width: 80px; margin-left: 8px;">`;
|
html += `<input type="number" id="rpg-history-message-count" min="0" max="50" value="${historyPersistence.messageCount}" class="rpg-input" style="width: 80px; margin-left: 8px;">`;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// Injection position
|
// Injection position
|
||||||
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
||||||
html += `<label for="rpg-history-injection-position">Injection Position:</label>`;
|
html += `<label for="rpg-history-injection-position">${i18n.getTranslation('historyPersistence.injectionPosition') || 'Injection Position:'}</label>`;
|
||||||
html += `<select id="rpg-history-injection-position" class="rpg-select" style="margin-left: 8px;">`;
|
html += `<select id="rpg-history-injection-position" class="rpg-select" style="margin-left: 8px;">`;
|
||||||
html += `<option value="user_message_end" ${historyPersistence.injectionPosition === 'user_message_end' ? 'selected' : ''}>End of the User's Message</option>`;
|
html += `<option value="user_message_end" ${historyPersistence.injectionPosition === 'user_message_end' ? 'selected' : ''}>${i18n.getTranslation('historyPersistence.injectionPosition.userMessageEnd') || 'End of the User\'s Message'}</option>`;
|
||||||
html += `<option value="assistant_message_end" ${historyPersistence.injectionPosition === 'assistant_message_end' ? 'selected' : ''}>End of the Assistant's Message</option>`;
|
html += `<option value="assistant_message_end" ${historyPersistence.injectionPosition === 'assistant_message_end' ? 'selected' : ''}>${i18n.getTranslation('historyPersistence.injectionPosition.assistantMessageEnd') || 'End of the Assistant\'s Message'}</option>`;
|
||||||
html += `</select>`;
|
html += `</select>`;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// Custom preamble
|
// Custom preamble
|
||||||
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
|
||||||
html += `<label for="rpg-history-context-preamble">Custom Context Preamble:</label>`;
|
html += `<label for="rpg-history-context-preamble">${i18n.getTranslation('historyPersistence.customContextPreamble') || 'Custom Context Preamble:'}</label>`;
|
||||||
html += `<input type="text" id="rpg-history-context-preamble" value="${historyPersistence.contextPreamble || ''}" class="rpg-text-input" placeholder="Context for that moment:" style="width: 100%; margin-top: 4px;">`;
|
html += `<input type="text" id="rpg-history-context-preamble" value="${historyPersistence.contextPreamble || ''}" class="rpg-text-input" placeholder="${i18n.getTranslation('historyPersistence.customContextPreamblePlaceholder') || 'Context for that moment:'}" style="width: 100%; margin-top: 4px;">`;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// User Stats section - which stats to persist
|
// User Stats section - which stats to persist
|
||||||
html += `<h4 style="margin-top: 20px;"><i class="fa-solid fa-heart-pulse"></i> User Stats</h4>`;
|
html += `<h4 style="margin-top: 20px;"><i class="fa-solid fa-heart-pulse"></i> ${i18n.getTranslation('historyPersistence.userStatsSection') || 'User Stats'}</h4>`;
|
||||||
html += `<p class="rpg-editor-hint">Select which stats should be included in historical messages.</p>`;
|
html += `<p class="rpg-editor-hint">${i18n.getTranslation('historyPersistence.userStatsHint') || 'Select which stats should be included in historical messages.'}</p>`;
|
||||||
|
|
||||||
// Custom stats
|
// Custom stats
|
||||||
html += '<div class="rpg-history-persist-list">';
|
html += '<div class="rpg-history-persist-list">';
|
||||||
@@ -1528,7 +1567,7 @@ function renderHistoryPersistenceTab() {
|
|||||||
html += `
|
html += `
|
||||||
<div class="rpg-editor-toggle-row">
|
<div class="rpg-editor-toggle-row">
|
||||||
<input type="checkbox" id="rpg-history-status" ${userStatsConfig.statusSection.persistInHistory ? 'checked' : ''}>
|
<input type="checkbox" id="rpg-history-status" ${userStatsConfig.statusSection.persistInHistory ? 'checked' : ''}>
|
||||||
<label for="rpg-history-status">Status (Mood/Conditions)</label>
|
<label for="rpg-history-status">${i18n.getTranslation('historyPersistence.statusSection') || 'Status (Mood/Conditions)'}</label>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -1538,7 +1577,7 @@ function renderHistoryPersistenceTab() {
|
|||||||
html += `
|
html += `
|
||||||
<div class="rpg-editor-toggle-row">
|
<div class="rpg-editor-toggle-row">
|
||||||
<input type="checkbox" id="rpg-history-skills" ${userStatsConfig.skillsSection.persistInHistory ? 'checked' : ''}>
|
<input type="checkbox" id="rpg-history-skills" ${userStatsConfig.skillsSection.persistInHistory ? 'checked' : ''}>
|
||||||
<label for="rpg-history-skills">${userStatsConfig.skillsSection.label || 'Skills'}</label>
|
<label for="rpg-history-skills">${userStatsConfig.skillsSection.label || i18n.getTranslation('historyPersistence.skills') || 'Skills'}</label>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -1547,7 +1586,7 @@ function renderHistoryPersistenceTab() {
|
|||||||
html += `
|
html += `
|
||||||
<div class="rpg-editor-toggle-row">
|
<div class="rpg-editor-toggle-row">
|
||||||
<input type="checkbox" id="rpg-history-inventory" ${userStatsConfig.inventoryPersistInHistory ? 'checked' : ''}>
|
<input type="checkbox" id="rpg-history-inventory" ${userStatsConfig.inventoryPersistInHistory ? 'checked' : ''}>
|
||||||
<label for="rpg-history-inventory">Inventory</label>
|
<label for="rpg-history-inventory">${i18n.getTranslation('historyPersistence.inventory') || 'Inventory'}</label>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -1555,14 +1594,14 @@ function renderHistoryPersistenceTab() {
|
|||||||
html += `
|
html += `
|
||||||
<div class="rpg-editor-toggle-row">
|
<div class="rpg-editor-toggle-row">
|
||||||
<input type="checkbox" id="rpg-history-quests" ${userStatsConfig.questsPersistInHistory ? 'checked' : ''}>
|
<input type="checkbox" id="rpg-history-quests" ${userStatsConfig.questsPersistInHistory ? 'checked' : ''}>
|
||||||
<label for="rpg-history-quests">Quests</label>
|
<label for="rpg-history-quests">${i18n.getTranslation('historyPersistence.quests') || 'Quests'}</label>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// Info Box section - which widgets to persist
|
// Info Box section - which widgets to persist
|
||||||
html += `<h4 style="margin-top: 20px;"><i class="fa-solid fa-info-circle"></i> Info Box</h4>`;
|
html += `<h4 style="margin-top: 20px;"><i class="fa-solid fa-info-circle"></i> ${i18n.getTranslation('historyPersistence.infoBoxSection') || 'Info Box'}</h4>`;
|
||||||
html += `<p class="rpg-editor-hint">Select which info box fields should be included in historical messages. These are recommended for time tracking.</p>`;
|
html += `<p class="rpg-editor-hint">${i18n.getTranslation('historyPersistence.infoBoxHint') || 'Select which info box fields should be included in historical messages. These are recommended for time tracking.'}</p>`;
|
||||||
|
|
||||||
html += '<div class="rpg-history-persist-list">';
|
html += '<div class="rpg-history-persist-list">';
|
||||||
const widgetLabels = {
|
const widgetLabels = {
|
||||||
@@ -1579,7 +1618,7 @@ function renderHistoryPersistenceTab() {
|
|||||||
html += `
|
html += `
|
||||||
<div class="rpg-editor-toggle-row">
|
<div class="rpg-editor-toggle-row">
|
||||||
<input type="checkbox" id="rpg-history-widget-${widgetId}" class="rpg-history-widget-toggle" data-widget="${widgetId}" ${widget.persistInHistory ? 'checked' : ''}>
|
<input type="checkbox" id="rpg-history-widget-${widgetId}" class="rpg-history-widget-toggle" data-widget="${widgetId}" ${widget.persistInHistory ? 'checked' : ''}>
|
||||||
<label for="rpg-history-widget-${widgetId}">${widgetLabels[widgetId] || widgetId}</label>
|
<label for="rpg-history-widget-${widgetId}">${i18n.getTranslation('historyPersistence.widget.' + widgetId) || widgetLabels[widgetId] || widgetId}</label>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -1587,8 +1626,8 @@ function renderHistoryPersistenceTab() {
|
|||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
// Present Characters section
|
// Present Characters section
|
||||||
html += `<h4 style="margin-top: 20px;"><i class="fa-solid fa-users"></i> Present Characters</h4>`;
|
html += `<h4 style="margin-top: 20px;"><i class="fa-solid fa-users"></i> ${i18n.getTranslation('historyPersistence.presentCharactersSection') || 'Present Characters'}</h4>`;
|
||||||
html += `<p class="rpg-editor-hint">Select which character fields should be included in historical messages.</p>`;
|
html += `<p class="rpg-editor-hint">${i18n.getTranslation('historyPersistence.presentCharactersHint') || 'Select which character fields should be included in historical messages.'}</p>`;
|
||||||
|
|
||||||
html += '<div class="rpg-history-persist-list">';
|
html += '<div class="rpg-history-persist-list">';
|
||||||
|
|
||||||
@@ -1609,7 +1648,7 @@ function renderHistoryPersistenceTab() {
|
|||||||
html += `
|
html += `
|
||||||
<div class="rpg-editor-toggle-row">
|
<div class="rpg-editor-toggle-row">
|
||||||
<input type="checkbox" id="rpg-history-thoughts" ${presentCharsConfig.thoughts.persistInHistory ? 'checked' : ''}>
|
<input type="checkbox" id="rpg-history-thoughts" ${presentCharsConfig.thoughts.persistInHistory ? 'checked' : ''}>
|
||||||
<label for="rpg-history-thoughts">${presentCharsConfig.thoughts.name || 'Thoughts'}</label>
|
<label for="rpg-history-thoughts">${presentCharsConfig.thoughts.name || i18n.getTranslation('historyPersistence.thoughts') || 'Thoughts'}</label>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,16 @@ export const WEATHER_PATTERNS_BY_LANGUAGE = {
|
|||||||
{ id: "sunny", patterns: [ "солнечно", "ясно", "ярко", "ясное утро", "ясный день" ] },
|
{ id: "sunny", patterns: [ "солнечно", "ясно", "ярко", "ясное утро", "ясный день" ] },
|
||||||
{ id: "none", 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: ["多云", "阴天", "室内", "屋内"] },
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function extractFieldValue(fieldValue) {
|
|||||||
export function toSnakeCase(name) {
|
export function toSnakeCase(name) {
|
||||||
return name
|
return name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/[^a-z0-9]+/g, '_')
|
.replace(/[^\p{L}\p{N}]+/gu, '_')
|
||||||
.replace(/^_+|_+$/g, '');
|
.replace(/^_+|_+$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +43,11 @@ export function namesMatch(cardName, aiName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isPresentCharactersEnabled() {
|
export function isPresentCharactersEnabled() {
|
||||||
return !!(extensionSettings.showCharacterThoughts || extensionSettings.showAlternatePresentCharactersPanel);
|
return !!(
|
||||||
|
extensionSettings.showCharacterThoughts
|
||||||
|
|| extensionSettings.showAlternatePresentCharactersPanel
|
||||||
|
|| extensionSettings.showThoughtsInChat
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPresentCharactersTrackerData({ useCommittedFallback = true } = {}) {
|
export function getPresentCharactersTrackerData({ useCommittedFallback = true } = {}) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
@@ -4720,6 +4720,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
|
|
||||||
.rpg-restore-prompt-btn {
|
.rpg-restore-prompt-btn {
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 0.5em 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Editor buttons */
|
/* Editor buttons */
|
||||||
@@ -9392,6 +9395,7 @@ body:has(.rpg-panel.rpg-mobile-open) .rpg-fab-widget-container {
|
|||||||
color: var(--SmartThemeBodyColor, #eaeaea);
|
color: var(--SmartThemeBodyColor, #eaeaea);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
@@ -10889,7 +10893,10 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
|
|||||||
|
|
||||||
/* Features row container */
|
/* Features row container */
|
||||||
.rpg-features-row {
|
.rpg-features-row {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-auto-columns: min-content;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
@@ -10900,11 +10907,11 @@ body[data-theme="cyberpunk"] .rpg-music-widget-play {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Center items when they fit, allow scrolling when they don't */
|
/* Center items when they fit, allow scrolling when they don't */
|
||||||
.rpg-features-row::before,
|
/*.rpg-features-row::before,*/
|
||||||
.rpg-features-row::after {
|
/*.rpg-features-row::after {*/
|
||||||
content: '';
|
/* content: '';*/
|
||||||
margin: auto;
|
/* margin: auto;*/
|
||||||
}
|
/*}*/
|
||||||
|
|
||||||
/* Hide scrollbar for cleaner look while maintaining functionality */
|
/* Hide scrollbar for cleaner look while maintaining functionality */
|
||||||
.rpg-features-row::-webkit-scrollbar {
|
.rpg-features-row::-webkit-scrollbar {
|
||||||
|
|||||||
+82
-76
@@ -1,6 +1,6 @@
|
|||||||
<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>
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<div class="rpg-strip-attributes-grid"></div>
|
<div class="rpg-strip-attributes-grid"></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Refresh Button (bottom) -->
|
<!-- Refresh Button (bottom) -->
|
||||||
<button id="rpg-strip-refresh" class="rpg-strip-refresh-btn" title="Refresh RPG Info">
|
<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>
|
<i class="fa-solid fa-sync"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,7 +114,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>
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
|
|
||||||
<!-- 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>
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
|
|
||||||
<!-- Deception System Toggle -->
|
<!-- Deception System Toggle -->
|
||||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-deception-toggle-wrapper">
|
<div class="rpg-toggle-container rpg-feature-col" id="rpg-deception-toggle-wrapper">
|
||||||
<label class="rpg-toggle-label" title="Deception System">
|
<label class="rpg-toggle-label" title="Deception System" data-i18n-title="template.mainPanel.deceptionSystem">
|
||||||
<input type="checkbox" id="rpg-toggle-deception">
|
<input type="checkbox" id="rpg-toggle-deception">
|
||||||
<i class="fa-solid fa-masks-theater"></i>
|
<i class="fa-solid fa-masks-theater"></i>
|
||||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.deceptionSystem">Deception System</span>
|
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.deceptionSystem">Deception System</span>
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
|
|
||||||
<!-- Omniscience Filter Toggle -->
|
<!-- Omniscience Filter Toggle -->
|
||||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-omniscience-toggle-wrapper">
|
<div class="rpg-toggle-container rpg-feature-col" id="rpg-omniscience-toggle-wrapper">
|
||||||
<label class="rpg-toggle-label" title="Omniscience Filter">
|
<label class="rpg-toggle-label" title="Omniscience Filter" data-i18n-title="template.mainPanel.omniscienceFilter">
|
||||||
<input type="checkbox" id="rpg-toggle-omniscience">
|
<input type="checkbox" id="rpg-toggle-omniscience">
|
||||||
<i class="fa-solid fa-eye-slash"></i>
|
<i class="fa-solid fa-eye-slash"></i>
|
||||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.omniscienceFilter">Omniscience Filter</span>
|
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.omniscienceFilter">Omniscience Filter</span>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
|
|
||||||
<!-- CYOA Toggle -->
|
<!-- CYOA Toggle -->
|
||||||
<div class="rpg-toggle-container rpg-feature-col" id="rpg-cyoa-toggle-wrapper">
|
<div class="rpg-toggle-container rpg-feature-col" id="rpg-cyoa-toggle-wrapper">
|
||||||
<label class="rpg-toggle-label" title="CYOA">
|
<label class="rpg-toggle-label" title="CYOA" data-i18n-title="template.mainPanel.cyoa">
|
||||||
<input type="checkbox" id="rpg-toggle-cyoa">
|
<input type="checkbox" id="rpg-toggle-cyoa">
|
||||||
<i class="fa-solid fa-list-ol"></i>
|
<i class="fa-solid fa-list-ol"></i>
|
||||||
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.cyoa">CYOA</span>
|
<span class="rpg-toggle-text" data-i18n-key="template.mainPanel.cyoa">CYOA</span>
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
|
|
||||||
<!-- 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>
|
||||||
@@ -168,7 +168,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>
|
||||||
@@ -176,7 +176,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>
|
||||||
@@ -184,7 +184,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>
|
||||||
@@ -497,17 +497,19 @@
|
|||||||
<div id="rpg-weather-suboptions" style="margin-left: 24px; margin-top: 8px;">
|
<div id="rpg-weather-suboptions" style="margin-left: 24px; margin-top: 8px;">
|
||||||
<label class="checkbox_label">
|
<label class="checkbox_label">
|
||||||
<input type="radio" name="rpg-weather-position" id="rpg-toggle-weather-background" />
|
<input type="radio" name="rpg-weather-position" id="rpg-toggle-weather-background" />
|
||||||
<span>Show in Background</span>
|
<span data-i18n-key="template.settingsModal.display.weatherPosition.background">Show in Background</span>
|
||||||
</label>
|
</label>
|
||||||
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;">
|
<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).
|
Display weather effects behind the chat (standard behavior).
|
||||||
</small>
|
</small>
|
||||||
|
|
||||||
<label class="checkbox_label">
|
<label class="checkbox_label">
|
||||||
<input type="radio" name="rpg-weather-position" id="rpg-toggle-weather-foreground" />
|
<input type="radio" name="rpg-weather-position" id="rpg-toggle-weather-foreground" />
|
||||||
<span>Show in Foreground</span>
|
<span data-i18n-key="template.settingsModal.display.weatherPosition.foreground">Show in Foreground</span>
|
||||||
</label>
|
</label>
|
||||||
<small style="display: block; margin-left: 24px; margin-top: -8px; color: #888; font-size: 11px;">
|
<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).
|
Display weather effects in front of the chat (experimental).
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -720,7 +722,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>
|
||||||
@@ -825,9 +827,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>
|
||||||
@@ -868,9 +871,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>
|
||||||
@@ -879,12 +882,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>
|
||||||
@@ -898,7 +901,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>
|
||||||
|
|
||||||
@@ -906,17 +909,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>
|
||||||
@@ -939,24 +942,24 @@
|
|||||||
<!-- Preset Management Section -->
|
<!-- Preset Management Section -->
|
||||||
<div class="rpg-preset-management">
|
<div class="rpg-preset-management">
|
||||||
<div class="rpg-preset-row">
|
<div class="rpg-preset-row">
|
||||||
<label for="rpg-preset-select">Preset:</label>
|
<label for="rpg-preset-select" data-i18n-key="preset.label">Preset:</label>
|
||||||
<select id="rpg-preset-select" class="rpg-select">
|
<select id="rpg-preset-select" class="rpg-select">
|
||||||
<!-- Options populated by JavaScript -->
|
<!-- Options populated by JavaScript -->
|
||||||
</select>
|
</select>
|
||||||
<button id="rpg-preset-new" class="rpg-btn-icon" type="button" title="Create New Preset">
|
<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>
|
<i class="fa-solid fa-plus"></i>
|
||||||
</button>
|
</button>
|
||||||
<button id="rpg-preset-default" class="rpg-btn-icon" type="button" title="Set as Default Preset">
|
<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>
|
<i class="fa-solid fa-star"></i>
|
||||||
</button>
|
</button>
|
||||||
<button id="rpg-preset-delete" class="rpg-btn-icon" type="button" title="Delete Current Preset">
|
<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>
|
<i class="fa-solid fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-preset-association-row">
|
<div class="rpg-preset-association-row">
|
||||||
<label class="checkbox_label">
|
<label class="checkbox_label">
|
||||||
<input type="checkbox" id="rpg-preset-associate">
|
<input type="checkbox" id="rpg-preset-associate">
|
||||||
<span>Use this preset for: <strong id="rpg-preset-entity-name">Character</strong></span>
|
<span data-i18n-key="preset.useThisPresetFor">Use this preset for: </span><strong id="rpg-preset-entity-name">Character</strong>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1021,210 +1024,213 @@
|
|||||||
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Deception System Prompt -->
|
<!-- Deception System Prompt -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-deception" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-deception" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-solid fa-masks-theater"></i> Deception System Prompt
|
<i class="fa-solid fa-masks-theater"></i> <span data-i18n-key="template.promptsEditor.deceptionPrompt.title">Deception System 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.deceptionPrompt.note">
|
||||||
Injected when "Enable Deception System" is enabled. Instructs AI to mark lies and deceptions with hidden tags.
|
Injected when "Enable Deception System" is enabled. Instructs AI to mark lies and deceptions with hidden tags.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-deception" class="rpg-prompt-textarea" rows="4"></textarea>
|
<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;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="deception" 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>
|
||||||
|
|
||||||
<!-- Omniscience Filter Prompt -->
|
<!-- Omniscience Filter Prompt -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-omniscience" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-omniscience" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-solid fa-eye-slash"></i> Omniscience Filter Prompt
|
<i class="fa-solid fa-eye-slash"></i> <span data-i18n-key="template.promptsEditor.omnisciencePrompt.title">Omniscience Filter 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.omnisciencePrompt.note">
|
||||||
Injected when "Enable Omniscience Filter" is enabled. Instructs AI to separate information the player character cannot perceive into hidden filter tags.
|
Injected when "Enable Omniscience Filter" is enabled. Instructs AI to separate information the player character cannot perceive into hidden <ofilter> tags.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-omniscience" class="rpg-prompt-textarea" rows="6"></textarea>
|
<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;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="omniscience" 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>
|
||||||
|
|
||||||
<!-- CYOA Prompt -->
|
<!-- CYOA Prompt -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-cyoa" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-cyoa" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-solid fa-list-ol"></i> CYOA Prompt
|
<i class="fa-solid fa-list-ol"></i> <span data-i18n-key="template.promptsEditor.cyoaPrompt.title">CYOA 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.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.
|
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>
|
</small>
|
||||||
<textarea id="rpg-prompt-cyoa" class="rpg-prompt-textarea" rows="4"></textarea>
|
<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;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="cyoa" 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>
|
||||||
|
|
||||||
<!-- 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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Context Instructions Prompt -->
|
<!-- Context Instructions Prompt -->
|
||||||
<div class="rpg-prompt-editor-section">
|
<div class="rpg-prompt-editor-section">
|
||||||
<label for="rpg-prompt-context-instructions" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
<label for="rpg-prompt-context-instructions" style="display: block; margin-bottom: 8px; font-weight: 600;">
|
||||||
<i class="fa-solid fa-comment-dots"></i> Context Instructions Prompt
|
<i class="fa-solid fa-comment-dots"></i> <span data-i18n-key="template.promptsEditor.contextPrompt.title">Context Instructions 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.contextPrompt.note">
|
||||||
Injected in Separate/External mode after the context summary. Tells the AI how to use the context.
|
Injected in Separate/External mode after the context summary. Tells the AI how to use the context.
|
||||||
</small>
|
</small>
|
||||||
<textarea id="rpg-prompt-context-instructions" class="rpg-prompt-textarea" rows="4"></textarea>
|
<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;">
|
<button class="menu_button rpg-restore-prompt-btn" data-prompt="contextInstructions" 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>
|
||||||
|
|
||||||
<!-- 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user