From 96d589adc057631cce43d9aeb2f8f51ff51143bb Mon Sep 17 00:00:00 2001 From: dd178 <66549247+dd178@users.noreply.github.com> Date: Mon, 23 Mar 2026 03:27:12 +0800 Subject: [PATCH] =?UTF-8?q?```=20feat(encounter):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E6=88=98=E6=96=97=E9=81=AD=E9=81=87=E7=95=8C=E9=9D=A2=E5=9B=BD?= =?UTF-8?q?=E9=99=85=E5=8C=96=E6=94=AF=E6=8C=81=E5=92=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加新的中文翻译项包括战斗结果状态、错误消息、界面标签等 - 将硬编码的文本替换为国际化翻译调用 - 添加战斗遭遇初始化和处理过程中的错误处理消息 - 增加确认对话框的本地化文本 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样式属性 ``` --- src/i18n/zh-cn.json | 28 +++++++- src/systems/generation/jsonPromptHelpers.js | 2 +- src/systems/generation/lockManager.js | 2 +- src/systems/generation/parser.js | 2 +- src/systems/rendering/thoughts.js | 2 +- src/systems/rendering/userStats.js | 2 +- src/systems/ui/encounterUI.js | 73 ++++++++++++--------- src/systems/ui/weatherEffects.js | 10 +++ src/utils/jsonMigration.js | 2 +- style.css | 1 + 10 files changed, 85 insertions(+), 39 deletions(-) diff --git a/src/i18n/zh-cn.json b/src/i18n/zh-cn.json index 8f308bb..8c7e541 100644 --- a/src/i18n/zh-cn.json +++ b/src/i18n/zh-cn.json @@ -100,7 +100,7 @@ "template.settingsModal.advanced.externalApi.apiKeyNote": "您的外部服务 API 密钥。", "template.settingsModal.advanced.externalApi.model": "模型", "template.settingsModal.advanced.externalApi.modelNote": "模型标识符(例如 gpt-4o-mini、claude-3-haiku、mistral-7b)。", - "template.settingsModal.advanced.externalApi.maxTokens": "最大令牌数", + "template.settingsModal.advanced.externalApi.maxTokens": "最大token数", "template.settingsModal.advanced.externalApi.temperature": "温度", "template.settingsModal.advanced.externalApi.testConnection": "测试连接", "template.settingsModal.advanced.contextMessages": "上下文消息:", @@ -427,6 +427,7 @@ "encounter.ui.concludeEncounterTitle": "提前结束遭遇", "encounter.ui.closeTitle": "关闭(结束战斗)", "encounter.ui.initializingCombat": "正在初始化战斗...", + "encounter.ui.initializingCombatEncounter": "正在初始化战斗遭遇...", "encounter.ui.combatBegins": "战斗开始!", "encounter.ui.allEnemies": "所有敌人", "encounter.ui.areaOfEffect": "范围效果", @@ -448,6 +449,31 @@ "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": "确认", diff --git a/src/systems/generation/jsonPromptHelpers.js b/src/systems/generation/jsonPromptHelpers.js index 2d66deb..c7a7107 100644 --- a/src/systems/generation/jsonPromptHelpers.js +++ b/src/systems/generation/jsonPromptHelpers.js @@ -17,7 +17,7 @@ import { i18n } from '../../core/i18n.js'; function toSnakeCase(name) { return name .toLowerCase() - .replace(/[^a-z0-9]+/g, '_') + .replace(/[^\p{L}\p{N}]+/gu, '_') .replace(/^_+|_+$/g, ''); } diff --git a/src/systems/generation/lockManager.js b/src/systems/generation/lockManager.js index e8f467c..ca5f55d 100644 --- a/src/systems/generation/lockManager.js +++ b/src/systems/generation/lockManager.js @@ -251,7 +251,7 @@ function applyCharactersLocks(data, lockedItems) { // Use the same conversion as toSnakeCase in thoughts.js const snakeCaseFieldName = fieldName .toLowerCase() - .replace(/[^a-z0-9]+/g, '_') + .replace(/[^\p{L}\p{N}]+/gu, '_') .replace(/^_+|_+$/g, ''); let locked = false; diff --git a/src/systems/generation/parser.js b/src/systems/generation/parser.js index 59fdcf8..66edaf3 100644 --- a/src/systems/generation/parser.js +++ b/src/systems/generation/parser.js @@ -19,7 +19,7 @@ function toFieldKey(name) { const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim(); return baseName .toLowerCase() - .replace(/[^a-z0-9]+/g, '_') + .replace(/[^\p{L}\p{N}]+/gu, '_') .replace(/^_+|_+$/g, ''); } diff --git a/src/systems/rendering/thoughts.js b/src/systems/rendering/thoughts.js index 7cdae73..8e3b27d 100644 --- a/src/systems/rendering/thoughts.js +++ b/src/systems/rendering/thoughts.js @@ -115,7 +115,7 @@ function extractFieldValue(fieldValue) { function toSnakeCase(name) { return name .toLowerCase() - .replace(/[^a-z0-9]+/g, '_') + .replace(/[^\p{L}\p{N}]+/gu, '_') .replace(/^_+|_+$/g, ''); } diff --git a/src/systems/rendering/userStats.js b/src/systems/rendering/userStats.js index d82e3e9..755dd55 100644 --- a/src/systems/rendering/userStats.js +++ b/src/systems/rendering/userStats.js @@ -34,7 +34,7 @@ function toFieldKey(name) { const baseName = name.replace(/\s*\(.*\)\s*$/, '').trim(); return baseName .toLowerCase() - .replace(/[^a-z0-9]+/g, '_') + .replace(/[^\p{L}\p{N}]+/gu, '_') .replace(/^_+|_+$/g, ''); } diff --git a/src/systems/ui/encounterUI.js b/src/systems/ui/encounterUI.js index 5ae4372..203c4cc 100644 --- a/src/systems/ui/encounterUI.js +++ b/src/systems/ui/encounterUI.js @@ -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; } @@ -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 {
-

${combatData.environment || 'Battle Arena'}

+

${combatData.environment || i18n.getTranslation('encounter.ui.environment.default') || 'Battle Arena'}

-

Enemies

+

${i18n.getTranslation('encounter.ui.enemiesTitle') || 'Enemies'}

${this.renderEnemies(combatData.enemies)}
@@ -381,7 +381,7 @@ export class EncounterModal {
-

Party

+

${i18n.getTranslation('encounter.ui.partyTitle') || 'Party'}

${this.renderParty(combatData.party)}
@@ -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 {

${enemy.name}

- ${enemy.hp}/${enemy.maxHp} HP + ${enemy.hp}/${enemy.maxHp}${i18n.getTranslation('encounter.ui.hpSuffix') || ' HP'}
${enemy.statuses && enemy.statuses.length > 0 ? `
@@ -481,10 +481,10 @@ export class EncounterModal { ${member.name}
-

${member.name} ${member.isPlayer ? '(You)' : ''}

+

${member.name} ${member.isPlayer ? i18n.getTranslation('encounter.ui.playerSuffix') || '(You)' : ''}

- ${member.hp}/${member.maxHp} HP + ${member.hp}/${member.maxHp}${i18n.getTranslation('encounter.ui.hpSuffix') || ' HP'}
${member.statuses && member.statuses.length > 0 ? `
${member.statuses.map(status => `${status.emoji}`).join('')} @@ -585,7 +585,7 @@ export class EncounterModal {
${enemy.sprite || '👹'}
${enemy.name}
-
${enemy.hp}/${enemy.maxHp} HP
+
${enemy.hp}/${enemy.maxHp}${i18n.getTranslation('encounter.ui.hpSuffix') || ' HP'}
`; } @@ -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 {
${avatarIcon}
${member.name}${isPlayer}
-
${member.hp}/${member.maxHp} HP
+
${member.hp}/${member.maxHp}${i18n.getTranslation('encounter.ui.hpSuffix') || ' HP'}
`; } @@ -670,7 +670,7 @@ export class EncounterModal { return `
-

Your Actions

+

${i18n.getTranslation('encounter.ui.yourActions') || 'Your Actions'}

@@ -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'}"> ${attackName} ${typeIcon} `; @@ -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 => { @@ -1205,13 +1205,21 @@ 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 = `
-

${result}

+

${text}

${i18n.getTranslation('encounter.ui.generatingCombatSummary') || 'Generating combat summary...'}

@@ -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 = ` `; @@ -1328,7 +1337,7 @@ export class EncounterModal { ${i18n.getTranslation('encounter.ui.regenerate') || 'Regenerate'}
diff --git a/src/systems/ui/weatherEffects.js b/src/systems/ui/weatherEffects.js index 21e3139..0f76464 100644 --- a/src/systems/ui/weatherEffects.js +++ b/src/systems/ui/weatherEffects.js @@ -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: ["多云", "阴天", "室内", "屋内"] }, + ], } /** diff --git a/src/utils/jsonMigration.js b/src/utils/jsonMigration.js index c122b84..78412e7 100644 --- a/src/utils/jsonMigration.js +++ b/src/utils/jsonMigration.js @@ -102,7 +102,7 @@ export function migrateUserStatsToJSON(textData) { const statMatch = trimmed.match(/^-\s*([^:]+):\s*(\d+)%/); if (statMatch) { 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({ id: id, name: name, diff --git a/style.css b/style.css index bd7b3d2..e3a2e3a 100644 --- a/style.css +++ b/style.css @@ -9238,6 +9238,7 @@ body:has(.rpg-panel.rpg-mobile-open) .rpg-fab-widget-container { color: var(--SmartThemeBodyColor, #eaeaea); font-size: 14px; font-weight: 600; + white-space: nowrap; cursor: pointer; transition: all 0.2s ease; }