Merge upstream/main into doom-lite-expression-sync-v2

This commit is contained in:
Tremendoussly
2026-04-14 16:21:07 +02:00
38 changed files with 1922 additions and 477 deletions
+99 -90
View File
@@ -71,7 +71,7 @@ export class EncounterModal {
}
// Show loading state
this.showLoadingState('Initializing combat encounter...');
this.showLoadingState(i18n.getTranslation('encounter.ui.initializingCombatEncounter') || 'Initializing combat encounter...');
// Open the modal
this.modal.classList.add('is-open');
@@ -88,7 +88,7 @@ export class EncounterModal {
});
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;
}
@@ -96,7 +96,7 @@ export class EncounterModal {
const combatData = parseEncounterJSON(response);
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;
}
@@ -121,7 +121,7 @@ export class EncounterModal {
} catch (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 {
this.isInitializing = false;
}
@@ -142,94 +142,94 @@ export class EncounterModal {
<div class="rpg-encounter-overlay"></div>
<div class="rpg-encounter-container" style="max-width: 600px;">
<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 class="rpg-encounter-content" style="padding: 24px;">
<div class="rpg-narrative-config-section">
<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>
<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;">
<option value="present" ${combatDefaults.tense === 'present' ? 'selected' : ''}>Present</option>
<option value="past" ${combatDefaults.tense === 'past' ? 'selected' : ''}>Past</option>
<option value="present" ${combatDefaults.tense === 'present' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.present') || 'Present'}</option>
<option value="past" ${combatDefaults.tense === 'past' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.past') || 'Past'}</option>
</select>
</div>
<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;">
<option value="first" ${combatDefaults.person === 'first' ? 'selected' : ''}>First Person</option>
<option value="second" ${combatDefaults.person === 'second' ? 'selected' : ''}>Second Person</option>
<option value="third" ${combatDefaults.person === 'third' ? 'selected' : ''}>Third 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' : ''}>${i18n.getTranslation('encounter.configModal.options.secondPerson') || 'Second Person'}</option>
<option value="third" ${combatDefaults.person === 'third' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.thirdPerson') || 'Third Person'}</option>
</select>
</div>
<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;">
<option value="omniscient" ${combatDefaults.narration === 'omniscient' ? 'selected' : ''}>Omniscient</option>
<option value="limited" ${combatDefaults.narration === 'limited' ? 'selected' : ''}>Limited</option>
<option value="omniscient" ${combatDefaults.narration === 'omniscient' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.omniscient') || 'Omniscient'}</option>
<option value="limited" ${combatDefaults.narration === 'limited' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.limited') || 'Limited'}</option>
</select>
</div>
<div class="rpg-setting-row" style="margin-bottom: 12px;">
<label for="config-combat-pov" style="min-width: 100px;">Point of View:</label>
<input type="text" id="config-combat-pov" class="text_pole" placeholder="narrator" value="${combatDefaults.pov || ''}" style="flex: 1;" />
<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="${i18n.getTranslation('encounter.configModal.placeholders.narrator') || 'narrator'}" value="${combatDefaults.pov || ''}" style="flex: 1;" />
</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));">
<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>
<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;">
<option value="present" ${summaryDefaults.tense === 'present' ? 'selected' : ''}>Present</option>
<option value="past" ${summaryDefaults.tense === 'past' ? 'selected' : ''}>Past</option>
<option value="present" ${summaryDefaults.tense === 'present' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.present') || 'Present'}</option>
<option value="past" ${summaryDefaults.tense === 'past' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.past') || 'Past'}</option>
</select>
</div>
<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;">
<option value="first" ${summaryDefaults.person === 'first' ? 'selected' : ''}>First Person</option>
<option value="second" ${summaryDefaults.person === 'second' ? 'selected' : ''}>Second Person</option>
<option value="third" ${summaryDefaults.person === 'third' ? 'selected' : ''}>Third 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' : ''}>${i18n.getTranslation('encounter.configModal.options.secondPerson') || 'Second Person'}</option>
<option value="third" ${summaryDefaults.person === 'third' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.thirdPerson') || 'Third Person'}</option>
</select>
</div>
<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;">
<option value="omniscient" ${summaryDefaults.narration === 'omniscient' ? 'selected' : ''}>Omniscient</option>
<option value="limited" ${summaryDefaults.narration === 'limited' ? 'selected' : ''}>Limited</option>
<option value="omniscient" ${summaryDefaults.narration === 'omniscient' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.omniscient') || 'Omniscient'}</option>
<option value="limited" ${summaryDefaults.narration === 'limited' ? 'selected' : ''}>${i18n.getTranslation('encounter.configModal.options.limited') || 'Limited'}</option>
</select>
</div>
<div class="rpg-setting-row" style="margin-bottom: 12px;">
<label for="config-summary-pov" style="min-width: 100px;">Point of View:</label>
<input type="text" id="config-summary-pov" class="text_pole" placeholder="narrator" value="${summaryDefaults.pov || ''}" style="flex: 1;" />
<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="${i18n.getTranslation('encounter.configModal.placeholders.narrator') || 'narrator'}" value="${summaryDefaults.pov || ''}" style="flex: 1;" />
</div>
</div>
<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;">
<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>
</div>
<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;">
<i class="fa-solid fa-times"></i> Cancel
<i class="fa-solid fa-times"></i> ${i18n.getTranslation('global.cancel') || 'Cancel'}
</button>
<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>
</div>
</div>
@@ -303,12 +303,12 @@ export class EncounterModal {
<div class="rpg-encounter-overlay"></div>
<div class="rpg-encounter-container">
<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">
<button id="rpg-encounter-conclude" class="rpg-encounter-conclude-btn" title="Conclude encounter early">
<i class="fa-solid fa-flag-checkered"></i> Conclude Encounter
<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> ${i18n.getTranslation('encounter.ui.concludeEncounterButton') || 'Conclude Encounter'}
</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>
</button>
</div>
@@ -316,7 +316,7 @@ export class EncounterModal {
<div class="rpg-encounter-content">
<div id="rpg-encounter-loading" class="rpg-encounter-loading">
<i class="fa-solid fa-spinner fa-spin"></i>
<p>Initializing combat...</p>
<p>${i18n.getTranslation('encounter.ui.initializingCombat') || 'Initializing combat...'}</p>
</div>
<div id="rpg-encounter-main" class="rpg-encounter-main" style="display: none;">
<!-- Combat UI will be rendered here -->
@@ -331,20 +331,20 @@ export class EncounterModal {
// Add event listeners
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.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();
}
});
// Close on overlay 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();
}
});
@@ -368,12 +368,12 @@ export class EncounterModal {
<div class="rpg-encounter-battlefield">
<!-- 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>
<!-- Enemies 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">
${this.renderEnemies(combatData.enemies)}
</div>
@@ -381,7 +381,7 @@ export class EncounterModal {
<!-- Party 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">
${this.renderParty(combatData.party)}
</div>
@@ -389,10 +389,10 @@ export class EncounterModal {
<!-- Combat Log -->
<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 class="rpg-encounter-log-entry">
<em>Combat begins!</em>
<em>${i18n.getTranslation('encounter.ui.combatBegins') || 'Combat begins!'}</em>
</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)
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
const fallbackSvg = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2NjY2NjYyIgb3BhY2l0eT0iMC4zIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGR5PSIuM2VtIiBmaWxsPSIjNjY2IiBmb250LXNpemU9IjQwIj4/PC90ZXh0Pjwvc3ZnPg==';
@@ -434,7 +434,7 @@ export class EncounterModal {
<h4>${enemy.name}</h4>
<div class="rpg-encounter-hp-bar">
<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>
${enemy.statuses && enemy.statuses.length > 0 ? `
<div class="rpg-encounter-statuses">
@@ -481,10 +481,10 @@ export class EncounterModal {
<img src="${avatarUrl || fallbackSvg}" alt="${member.name}" onerror="this.src='${fallbackSvg}'">
</div>
<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-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 class="rpg-encounter-statuses">
${member.statuses.map(status => `<span class="rpg-encounter-status" title="${status.name}">${status.emoji}</span>`).join('')}
@@ -561,18 +561,18 @@ export class EncounterModal {
targetOptions = `
<div class="rpg-target-option" data-target="all-enemies">
<div class="rpg-target-icon">💥</div>
<div class="rpg-target-name">All Enemies</div>
<div class="rpg-target-desc">Area of Effect</div>
<div class="rpg-target-name">${i18n.getTranslation('encounter.ui.allEnemies') || 'All Enemies'}</div>
<div class="rpg-target-desc">${i18n.getTranslation('encounter.ui.areaOfEffect') || 'Area of Effect'}</div>
</div>
`;
} else if (attackType === 'both') {
targetOptions = `
<div class="rpg-target-option" data-target="all-enemies">
<div class="rpg-target-icon">💥</div>
<div class="rpg-target-name">All Enemies</div>
<div class="rpg-target-desc">Area of Effect</div>
<div class="rpg-target-name">${i18n.getTranslation('encounter.ui.allEnemies') || 'All Enemies'}</div>
<div class="rpg-target-desc">${i18n.getTranslation('encounter.ui.areaOfEffect') || 'Area of Effect'}</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-icon">${enemy.sprite || '👹'}</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>
`;
}
@@ -594,7 +594,7 @@ export class EncounterModal {
// Add party members (for heals/buffs)
combatStats.party.forEach((member, index) => {
if (member.hp > 0) {
const isPlayer = member.isPlayer ? ' (You)' : '';
const isPlayer = member.isPlayer ? i18n.getTranslation('encounter.ui.playerSuffix') || ' (You)' : '';
// Get avatar for party member
let avatarIcon = '✨';
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-icon">${avatarIcon}</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>
`;
}
@@ -618,11 +618,11 @@ export class EncounterModal {
targetModal.innerHTML = `
<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">
${targetOptions}
</div>
<button class="rpg-target-cancel">Cancel</button>
<button class="rpg-target-cancel">${i18n.getTranslation('global.cancel') || 'Cancel'}</button>
</div>
`;
@@ -661,7 +661,7 @@ export class EncounterModal {
renderPlayerControls(party, playerActions = null) {
const player = party.find(m => m.isPlayer);
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
@@ -670,11 +670,11 @@ export class EncounterModal {
return `
<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-button-group">
<h4>Attacks</h4>
<h4>${i18n.getTranslation('encounter.ui.attacks') || 'Attacks'}</h4>
${attacks.map(attack => {
// Support both old string format and new object format
const attackName = typeof attack === 'string' ? attack : attack.name;
@@ -686,7 +686,7 @@ export class EncounterModal {
data-action="attack"
data-value="${attackName}"
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}
</button>
`;
@@ -695,7 +695,7 @@ export class EncounterModal {
${items && items.length > 0 ? `
<div class="rpg-encounter-button-group">
<h4>Items</h4>
<h4>${i18n.getTranslation('encounter.ui.items') || 'Items'}</h4>
${items.map(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}
@@ -706,11 +706,11 @@ export class EncounterModal {
</div>
<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">
<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">
<i class="fa-solid fa-paper-plane"></i> Submit
<i class="fa-solid fa-paper-plane"></i> ${i18n.getTranslation('encounter.ui.submit') || 'Submit'}
</button>
</div>
</div>
@@ -746,15 +746,15 @@ export class EncounterModal {
if (!target) return;
if (target === 'all-enemies') {
actionText = `${userName} uses ${value} targeting all enemies!`;
actionText = `${userName} uses ${value}${i18n.getTranslation('encounter.ui.targetingAllEnemies') || ' targeting all enemies!'}`;
} else {
actionText = `${userName} uses ${value} on ${target}!`;
actionText = `${userName} uses ${value}${i18n.getTranslation('encounter.ui.on') || ' on '}${target}!`;
}
} else if (actionType === 'item') {
const target = await this.showTargetSelection('single-target', currentEncounter.combatStats);
if (!target) return;
actionText = `${userName} uses ${value} on ${target}!`;
actionText = `${userName} uses ${value}${i18n.getTranslation('encounter.ui.on') || ' on '}${target}!`;
}
await this.processCombatAction(actionText);
@@ -809,7 +809,7 @@ export class EncounterModal {
});
// 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
const actionPrompt = await buildCombatActionPrompt(action, currentEncounter.combatStats);
@@ -823,7 +823,7 @@ export class EncounterModal {
});
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;
}
@@ -831,7 +831,7 @@ export class EncounterModal {
const result = parseEncounterJSON(response);
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;
}
@@ -899,7 +899,7 @@ export class EncounterModal {
} catch (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
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 (controlsContainer) {
controlsContainer.innerHTML = '<p class="rpg-encounter-defeated">You have been defeated...</p>';
controlsContainer.innerHTML = '<p class="rpg-encounter-defeated">' + (i18n.getTranslation('encounter.ui.youHaveBeenDefeated') || 'You have been defeated...') + '</p>';
}
} else if (currentEncounter.playerActions && controlsContainer) {
// Check if actions have changed by comparing with previous state
@@ -1205,17 +1205,25 @@ export class EncounterModal {
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 color = resultColors[result] || '#888';
const text = resultTexts[result] || result;
mainContent.innerHTML = `
<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>
<h2 style="font-size: 32px; margin-bottom: 16px; text-transform: uppercase;">${result}</h2>
<p style="font-size: 18px; margin-bottom: 32px; opacity: 0.8;">Generating combat summary...</p>
<h2 style="font-size: 32px; margin-bottom: 16px; text-transform: uppercase;">${text}</h2>
<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;">
<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>
`;
@@ -1234,12 +1242,13 @@ export class EncounterModal {
if (!overScreen) return;
if (success) {
overScreen.querySelector('p').textContent = speakerName
? `Combat summary has been added to the chat by ${speakerName}.`
: 'Combat summary has been added to the chat.';
const message = speakerName
? (i18n.getTranslation('encounter.ui.combatSummaryAddedBy') || 'Combat summary has been added to the chat by {speakerName}.').replace('{speakerName}', speakerName)
: (i18n.getTranslation('encounter.ui.combatSummaryAdded') || 'Combat summary has been added to the chat.');
overScreen.querySelector('p').textContent = message;
overScreen.querySelector('.rpg-encounter-loading').innerHTML = `
<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>
`;
@@ -1251,11 +1260,11 @@ export class EncounterModal {
});
}
} 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 = `
<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;">
<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>
`;
@@ -1321,14 +1330,14 @@ export class EncounterModal {
loadingContent.innerHTML = `
<div class="rpg-encounter-error-box">
<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>
<div style="display: flex; gap: 1em;">
<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 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>
</div>
</div>
+1 -1
View File
@@ -144,7 +144,7 @@ export function updateCollapseToggleIcon() {
*/
export function setupCollapseToggle() {
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 $icon = $collapseToggle.find('i');
+20 -7
View File
@@ -38,10 +38,23 @@ export function updateMobileTabLabels() {
}
if (translationKey) {
const translation = i18n.getTranslation(translationKey);
if (translation) {
$tab.find('span').text(translation);
let fallback = '';
switch (tabName) {
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)
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)
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
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
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>');
+1 -1
View File
@@ -324,7 +324,7 @@ export function setupDiceRoller() {
e.stopPropagation(); // Prevent opening the dice popup
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;
}
+84 -45
View File
@@ -31,6 +31,7 @@ import { renderUserStats } from '../rendering/userStats.js';
import { renderInfoBox } from '../rendering/infoBox.js';
import { renderThoughts } from '../rendering/thoughts.js';
import { updateFabWidgets } from './mobile.js';
import { safeToSnake } from '../../utils/transformations.js';
let $editorModal = null;
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 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
*/
@@ -225,7 +256,7 @@ function updatePresetUI() {
// Update the default button appearance
const $defaultBtn = $('#rpg-preset-default');
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 {
$defaultBtn.removeClass('rpg-btn-active').attr('title', 'Set as Default Preset');
}
@@ -334,18 +365,18 @@ function resetToDefaults() {
extensionSettings.trackerConfig = {
userStats: {
customStats: [
{ id: 'health', name: i18n.getTranslation('stats.health'), enabled: true, persistInHistory: false },
{ id: 'satiety', name: i18n.getTranslation('stats.satiety'), enabled: true, persistInHistory: false },
{ id: 'energy', name: i18n.getTranslation('stats.energy'), enabled: true, persistInHistory: false },
{ id: 'hygiene', name: i18n.getTranslation('stats.hygiene'), enabled: true, persistInHistory: false },
{ id: 'arousal', name: i18n.getTranslation('stats.arousal'), enabled: true, persistInHistory: false }
{ id: 'health', name: i18n.getTranslation('stats.health') || 'Health', enabled: true, persistInHistory: false },
{ id: 'satiety', name: i18n.getTranslation('stats.satiety') || 'Satiety', enabled: true, persistInHistory: false },
{ id: 'energy', name: i18n.getTranslation('stats.energy') || 'Energy', enabled: true, persistInHistory: false },
{ id: 'hygiene', name: i18n.getTranslation('stats.hygiene') || 'Hygiene', enabled: true, persistInHistory: false },
{ id: 'arousal', name: i18n.getTranslation('stats.arousal') || 'Arousal', enabled: true, persistInHistory: false }
],
showRPGAttributes: true,
rpgAttributes: [
{ id: 'str', name: i18n.getTranslation('stats.str'), enabled: true, persistInHistory: false },
{ id: 'dex', name: i18n.getTranslation('stats.dex'), enabled: true, persistInHistory: false },
{ id: 'con', name: i18n.getTranslation('stats.con'), enabled: true, persistInHistory: false },
{ id: 'int', name: i18n.getTranslation('stats.int'), enabled: true, persistInHistory: false },
{ id: 'str', name: i18n.getTranslation('stats.str') || 'STR', enabled: true, persistInHistory: false },
{ id: 'dex', name: i18n.getTranslation('stats.dex') || 'DEX', enabled: true, persistInHistory: false },
{ id: 'con', name: i18n.getTranslation('stats.con') || 'CON', 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: 'cha', name: i18n.getTranslation('stats.cha'), enabled: true, persistInHistory: false }
],
@@ -635,7 +666,7 @@ function showImportModeDialog(migratedConfig, suggestedName, historyPersistence
</button>
<button id="rpg-import-as-new" class="rpg-btn-primary">
<i class="fa-solid fa-plus"></i>
Create New Preset
${i18n.getTranslation('preset.createNewPresetTitle') || 'Create New Preset'}
</button>
</div>
<button id="rpg-import-cancel" class="rpg-btn-cancel">Cancel</button>
@@ -728,15 +759,15 @@ function renderUserStatsTab() {
let html = '<div class="rpg-editor-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
const statsDisplayMode = config.statsDisplayMode || 'percentage';
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 += `<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="number" ${statsDisplayMode === 'number' ? 'checked' : ''}> Number</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' : ''}> ${i18n.getTranslation('stats.displayMode.number') || 'Number'}</label>`;
html += '</div>';
html += '</div>';
@@ -772,7 +803,7 @@ function renderUserStatsTab() {
const showLevel = config.showLevel !== undefined ? config.showLevel : true;
html += '<div class="rpg-editor-toggle-row">';
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>';
// Always send attributes toggle
@@ -885,7 +916,9 @@ function setupUserStatsListeners() {
// Rename stat
$('.rpg-stat-name').off('blur').on('blur', function () {
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
@@ -943,7 +976,9 @@ function setupUserStatsListeners() {
// Rename attribute
$('.rpg-attr-name').off('blur').on('blur', function () {
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
@@ -1006,8 +1041,8 @@ function renderInfoBoxTab() {
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 += '<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="Day (Numerical), Month, Year" ${config.widgets.date.format === 'Day (Numerical), Month, Year' ? 'selected' : ''}>Day (Numerical), 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' : ''}>${i18n.getTranslation('dateFormat.dayNumericalMonthYear') || 'Day (Numerical), Month, Year'}</option>`;
html += '</select>';
html += '</div>';
@@ -1394,7 +1429,9 @@ function setupPresentCharactersListeners() {
// Rename field
$('.rpg-field-label').off('blur').on('blur', function () {
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
@@ -1443,7 +1480,9 @@ function setupPresentCharactersListeners() {
// Rename character stat
$('.rpg-char-stat-label').off('blur').on('blur', function () {
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">';
// Main toggle and settings
html += `<h4><i class="fa-solid fa-clock-rotate-left"></i> 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 += `<h4><i class="fa-solid fa-clock-rotate-left"></i> ${i18n.getTranslation('historyPersistence.settingsTitle') || 'History Persistence Settings'}</h4>`;
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
html += '<div class="rpg-editor-toggle-row">';
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>';
// External API Only toggle - only show for separate/external modes
if (generationMode === 'separate' || generationMode === 'external') {
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 += `<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 += `<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
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 += '</div>';
// Injection position
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 += `<option value="user_message_end" ${historyPersistence.injectionPosition === 'user_message_end' ? 'selected' : ''}>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="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' : ''}>${i18n.getTranslation('historyPersistence.injectionPosition.assistantMessageEnd') || 'End of the Assistant\'s Message'}</option>`;
html += `</select>`;
html += '</div>';
// Custom preamble
html += '<div class="rpg-editor-input-row" style="margin-top: 12px;">';
html += `<label for="rpg-history-context-preamble">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 += `<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="${i18n.getTranslation('historyPersistence.customContextPreamblePlaceholder') || 'Context for that moment:'}" style="width: 100%; margin-top: 4px;">`;
html += '</div>';
// 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 += `<p class="rpg-editor-hint">Select which stats should be included in historical messages.</p>`;
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">${i18n.getTranslation('historyPersistence.userStatsHint') || 'Select which stats should be included in historical messages.'}</p>`;
// Custom stats
html += '<div class="rpg-history-persist-list">';
@@ -1528,7 +1567,7 @@ function renderHistoryPersistenceTab() {
html += `
<div class="rpg-editor-toggle-row">
<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>
`;
}
@@ -1538,7 +1577,7 @@ function renderHistoryPersistenceTab() {
html += `
<div class="rpg-editor-toggle-row">
<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>
`;
}
@@ -1547,7 +1586,7 @@ function renderHistoryPersistenceTab() {
html += `
<div class="rpg-editor-toggle-row">
<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>
`;
@@ -1555,14 +1594,14 @@ function renderHistoryPersistenceTab() {
html += `
<div class="rpg-editor-toggle-row">
<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>
`;
html += '</div>';
// 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 += `<p class="rpg-editor-hint">Select which info box fields should be included in historical messages. These are recommended for time tracking.</p>`;
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">${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">';
const widgetLabels = {
@@ -1579,7 +1618,7 @@ function renderHistoryPersistenceTab() {
html += `
<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' : ''}>
<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>
`;
}
@@ -1587,8 +1626,8 @@ function renderHistoryPersistenceTab() {
html += '</div>';
// Present Characters section
html += `<h4 style="margin-top: 20px;"><i class="fa-solid fa-users"></i> Present Characters</h4>`;
html += `<p class="rpg-editor-hint">Select which character fields should be included in historical messages.</p>`;
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">${i18n.getTranslation('historyPersistence.presentCharactersHint') || 'Select which character fields should be included in historical messages.'}</p>`;
html += '<div class="rpg-history-persist-list">';
@@ -1609,7 +1648,7 @@ function renderHistoryPersistenceTab() {
html += `
<div class="rpg-editor-toggle-row">
<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>
`;
}
+10
View File
@@ -129,6 +129,16 @@ export const WEATHER_PATTERNS_BY_LANGUAGE = {
{ id: "sunny", patterns: [ "солнечно", "ясно", "ярко", "ясное утро", "ясный день" ] },
{ id: "none", patterns: [ "облачно", "пасмурно", "в помещении", "внутри" ] },
],
"zh-cn": [
{ id: "blizzard", patterns: ["暴风雪"] },
{ id: "storm", patterns: ["风暴", "雷暴", "雷电"] },
{ id: "wind", patterns: ["风", "微风", "阵风", "大风"] },
{ id: "snow", patterns: ["雪", "小雪"] },
{ id: "rain", patterns: ["雨", "毛毛雨", "阵雨"] },
{ id: "mist", patterns: ["薄雾", "雾", "霾"] },
{ id: "sunny", patterns: ["晴朗", "晴天", "阳光明媚"] },
{ id: "none", patterns: ["多云", "阴天", "室内", "屋内"] },
],
}
/**