feat(encounter): 添加战斗遭遇界面国际化支持和优化错误处理

- 添加新的中文翻译项包括战斗结果状态、错误消息、界面标签等
- 将硬编码的文本替换为国际化翻译调用
- 添加战斗遭遇初始化和处理过程中的错误处理消息
- 增加确认对话框的本地化文本

fix(regex): 更新正则表达式以支持Unicode字符

- 将多个文件中的ASCII限定正则表达式 /[^a-z0-9]+/g 替换为Unicode感知的
  /[^\p{L}\p{N}]+/gu 以正确处理非ASCII字符
- 修复jsonMigration.js中的字符过滤逻辑

feat(weather): 为中文添加天气模式识别规则

- 在WEATHER_PATTERNS_BY_LANGUAGE中为zh-cn语言添加完整的天气关键词模式
- 支持中文天气条件的自动识别和效果应用

style(fab): 添加nowrap样式防止文本换行

- 在FAB组件中添加white-space: nowrap样式属性
```
This commit is contained in:
dd178
2026-03-23 03:27:12 +08:00
parent 55aa2a1e6a
commit 96d589adc0
10 changed files with 85 additions and 39 deletions
+27 -1
View File
@@ -100,7 +100,7 @@
"template.settingsModal.advanced.externalApi.apiKeyNote": "您的外部服务 API 密钥。", "template.settingsModal.advanced.externalApi.apiKeyNote": "您的外部服务 API 密钥。",
"template.settingsModal.advanced.externalApi.model": "模型", "template.settingsModal.advanced.externalApi.model": "模型",
"template.settingsModal.advanced.externalApi.modelNote": "模型标识符(例如 gpt-4o-mini、claude-3-haiku、mistral-7b)。", "template.settingsModal.advanced.externalApi.modelNote": "模型标识符(例如 gpt-4o-mini、claude-3-haiku、mistral-7b)。",
"template.settingsModal.advanced.externalApi.maxTokens": "最大令牌数", "template.settingsModal.advanced.externalApi.maxTokens": "最大token数",
"template.settingsModal.advanced.externalApi.temperature": "温度", "template.settingsModal.advanced.externalApi.temperature": "温度",
"template.settingsModal.advanced.externalApi.testConnection": "测试连接", "template.settingsModal.advanced.externalApi.testConnection": "测试连接",
"template.settingsModal.advanced.contextMessages": "上下文消息:", "template.settingsModal.advanced.contextMessages": "上下文消息:",
@@ -427,6 +427,7 @@
"encounter.ui.concludeEncounterTitle": "提前结束遭遇", "encounter.ui.concludeEncounterTitle": "提前结束遭遇",
"encounter.ui.closeTitle": "关闭(结束战斗)", "encounter.ui.closeTitle": "关闭(结束战斗)",
"encounter.ui.initializingCombat": "正在初始化战斗...", "encounter.ui.initializingCombat": "正在初始化战斗...",
"encounter.ui.initializingCombatEncounter": "正在初始化战斗遭遇...",
"encounter.ui.combatBegins": "战斗开始!", "encounter.ui.combatBegins": "战斗开始!",
"encounter.ui.allEnemies": "所有敌人", "encounter.ui.allEnemies": "所有敌人",
"encounter.ui.areaOfEffect": "范围效果", "encounter.ui.areaOfEffect": "范围效果",
@@ -448,6 +449,31 @@
"encounter.ui.submit": "提交", "encounter.ui.submit": "提交",
"encounter.ui.regenerate": "重新生成", "encounter.ui.regenerate": "重新生成",
"encounter.ui.or": "或", "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.locked": "已锁定",
"global.unlocked": "已解锁", "global.unlocked": "已解锁",
"global.confirm": "确认", "global.confirm": "确认",
+1 -1
View File
@@ -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, '');
} }
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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, '');
} }
+1 -1
View File
@@ -115,7 +115,7 @@ function extractFieldValue(fieldValue) {
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, '');
} }
+1 -1
View File
@@ -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, '');
} }
+41 -32
View File
@@ -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;
} }
@@ -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>
@@ -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('')}
@@ -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>
`; `;
} }
@@ -670,7 +670,7 @@ 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">
@@ -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>
`; `;
@@ -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 => {
@@ -1205,13 +1205,21 @@ 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;">${i18n.getTranslation('encounter.ui.generatingCombatSummary') || '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>
@@ -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>
`; `;
@@ -1328,7 +1337,7 @@ export class EncounterModal {
<i class="fa-solid fa-rotate-right"></i> ${i18n.getTranslation('encounter.ui.regenerate') || '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>
+10
View File
@@ -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: ["多云", "阴天", "室内", "屋内"] },
],
} }
/** /**
+1 -1
View File
@@ -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,
+1
View File
@@ -9238,6 +9238,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;
} }