96d589adc0
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样式属性
```
467 lines
18 KiB
JavaScript
467 lines
18 KiB
JavaScript
/**
|
|
* Lock Manager
|
|
* Handles applying and removing locks for tracker items
|
|
* Locks prevent AI from modifying specific values
|
|
*/
|
|
|
|
import { extensionSettings } from '../../core/state.js';
|
|
import { repairJSON } from '../../utils/jsonRepair.js';
|
|
|
|
/**
|
|
* Apply locks to tracker data before sending to AI.
|
|
* Adds "locked": true to locked items in JSON format.
|
|
*
|
|
* @param {string} trackerData - JSON string of tracker data
|
|
* @param {string} trackerType - Type of tracker ('userStats', 'infoBox', 'characters')
|
|
* @returns {string} Tracker data with locks applied
|
|
*/
|
|
export function applyLocks(trackerData, trackerType) {
|
|
if (!trackerData) return trackerData;
|
|
|
|
// Try to parse as JSON
|
|
const parsed = repairJSON(trackerData);
|
|
if (!parsed) {
|
|
// Not JSON format, return as-is (text format doesn't support locks)
|
|
return trackerData;
|
|
}
|
|
|
|
// Get locked items for this tracker type
|
|
const lockedItems = extensionSettings.lockedItems?.[trackerType] || {};
|
|
|
|
// Apply locks based on tracker type
|
|
switch (trackerType) {
|
|
case 'userStats':
|
|
return applyUserStatsLocks(parsed, lockedItems);
|
|
case 'infoBox':
|
|
return applyInfoBoxLocks(parsed, lockedItems);
|
|
case 'characters':
|
|
return applyCharactersLocks(parsed, lockedItems);
|
|
default:
|
|
return trackerData;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply locks to User Stats tracker
|
|
* @param {Object} data - Parsed user stats data
|
|
* @param {Object} lockedItems - Locked items configuration
|
|
* @returns {string} JSON string with locks applied
|
|
*/
|
|
function applyUserStatsLocks(data, lockedItems) {
|
|
// Lock individual stats within stats object
|
|
if (data.stats && lockedItems.stats) {
|
|
// Handle both section lock and individual stat locks
|
|
const isStatsLocked = lockedItems.stats === true;
|
|
if (isStatsLocked) {
|
|
// Lock entire stats section
|
|
for (const statName in data.stats) {
|
|
data.stats[statName] = {
|
|
value: data.stats[statName].value || data.stats[statName],
|
|
locked: true
|
|
};
|
|
}
|
|
} else {
|
|
// Lock individual stats
|
|
for (const statName in lockedItems.stats) {
|
|
if (lockedItems.stats[statName] && data.stats[statName] !== undefined) {
|
|
data.stats[statName] = {
|
|
value: data.stats[statName].value || data.stats[statName],
|
|
locked: true
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lock status field
|
|
if (data.status && lockedItems.status) {
|
|
data.status = {
|
|
...data.status,
|
|
locked: true
|
|
};
|
|
}
|
|
|
|
// Lock individual skills
|
|
if (data.skills && lockedItems.skills) {
|
|
if (Array.isArray(data.skills)) {
|
|
data.skills = data.skills.map(skill => {
|
|
if (typeof skill === 'string') {
|
|
if (lockedItems.skills[skill]) {
|
|
return { name: skill, locked: true };
|
|
}
|
|
return skill;
|
|
} else if (skill.name && lockedItems.skills[skill.name]) {
|
|
return { ...skill, locked: true };
|
|
}
|
|
return skill;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Lock inventory items - match by item name instead of index
|
|
if (data.inventory && lockedItems.inventory) {
|
|
// Helper function to apply locks based on item name
|
|
const applyInventoryLocks = (items, category) => {
|
|
if (!Array.isArray(items)) return items;
|
|
if (!lockedItems.inventory[category]) return items;
|
|
|
|
return items.map((item) => {
|
|
// Get item name (handle both string and object formats)
|
|
const itemName = typeof item === 'string' ? item : (item.item || item.name || '');
|
|
|
|
// Check if this specific item name is locked
|
|
if (lockedItems.inventory[category][itemName]) {
|
|
return typeof item === 'string'
|
|
? { item, locked: true }
|
|
: { ...item, locked: true };
|
|
}
|
|
return item;
|
|
});
|
|
};
|
|
|
|
// Apply locks to onPerson items
|
|
if (data.inventory.onPerson) {
|
|
data.inventory.onPerson = applyInventoryLocks(data.inventory.onPerson, 'onPerson');
|
|
}
|
|
|
|
// Apply locks to clothing items
|
|
if (data.inventory.clothing) {
|
|
data.inventory.clothing = applyInventoryLocks(data.inventory.clothing, 'clothing');
|
|
}
|
|
|
|
// Apply locks to assets
|
|
if (data.inventory.assets) {
|
|
data.inventory.assets = applyInventoryLocks(data.inventory.assets, 'assets');
|
|
}
|
|
|
|
// Apply locks to stored items - match by item name
|
|
if (data.inventory.stored && lockedItems.inventory.stored) {
|
|
for (const location in data.inventory.stored) {
|
|
if (Array.isArray(data.inventory.stored[location]) && lockedItems.inventory.stored[location]) {
|
|
data.inventory.stored[location] = data.inventory.stored[location].map((item) => {
|
|
const itemName = typeof item === 'string' ? item : (item.item || item.name || '');
|
|
if (lockedItems.inventory.stored[location][itemName]) {
|
|
return typeof item === 'string'
|
|
? { item, locked: true }
|
|
: { ...item, locked: true };
|
|
}
|
|
return item;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lock individual quests - handle paths like "quests.main" and "quests.optional[0]"
|
|
if (data.quests && lockedItems.quests) {
|
|
// Check if main quest is locked (entire section)
|
|
if (data.quests.main && lockedItems.quests.main === true) {
|
|
data.quests.main = { value: data.quests.main, locked: true };
|
|
}
|
|
|
|
// Check individual optional quests
|
|
if (data.quests.optional && Array.isArray(data.quests.optional)) {
|
|
data.quests.optional = data.quests.optional.map((quest, index) => {
|
|
const bracketPath = `optional[${index}]`;
|
|
if (lockedItems.quests[bracketPath]) {
|
|
return typeof quest === 'string'
|
|
? { title: quest, locked: true }
|
|
: { ...quest, locked: true };
|
|
}
|
|
return quest;
|
|
});
|
|
}
|
|
}
|
|
|
|
return JSON.stringify(data, null, 2);
|
|
}
|
|
|
|
/**
|
|
* Apply locks to Info Box tracker
|
|
* @param {Object} data - Parsed info box data
|
|
* @param {Object} lockedItems - Locked items configuration
|
|
* @returns {string} JSON string with locks applied
|
|
*/
|
|
function applyInfoBoxLocks(data, lockedItems) {
|
|
if (lockedItems.date && data.date) {
|
|
data.date = { ...data.date, locked: true };
|
|
}
|
|
|
|
if (lockedItems.weather && data.weather) {
|
|
data.weather = { ...data.weather, locked: true };
|
|
}
|
|
|
|
if (lockedItems.temperature && data.temperature) {
|
|
data.temperature = { ...data.temperature, locked: true };
|
|
}
|
|
|
|
if (lockedItems.time && data.time) {
|
|
data.time = { ...data.time, locked: true };
|
|
}
|
|
|
|
if (lockedItems.location && data.location) {
|
|
data.location = { ...data.location, locked: true };
|
|
}
|
|
|
|
if (lockedItems.recentEvents && data.recentEvents) {
|
|
data.recentEvents = { ...data.recentEvents, locked: true };
|
|
}
|
|
|
|
return JSON.stringify(data, null, 2);
|
|
}
|
|
|
|
/**
|
|
* Apply locks to Characters tracker
|
|
* @param {Object} data - Parsed characters data
|
|
* @param {Object} lockedItems - Locked items configuration
|
|
* @returns {string} JSON string with locks applied
|
|
*/
|
|
function applyCharactersLocks(data, lockedItems) {
|
|
// console.log('[Lock Manager] applyCharactersLocks called');
|
|
// console.log('[Lock Manager] Locked items:', JSON.stringify(lockedItems, null, 2));
|
|
// console.log('[Lock Manager] Input data:', JSON.stringify(data, null, 2));
|
|
|
|
// Handle both array format and object format
|
|
let characters = Array.isArray(data) ? data : (data.characters || []);
|
|
|
|
characters = characters.map((char, index) => {
|
|
const charName = char.name || char.characterName;
|
|
|
|
// Check if entire character is locked (index-based)
|
|
if (lockedItems[index] === true) {
|
|
// console.log('[Lock Manager] Locking entire character by index:', index);
|
|
return { ...char, locked: true };
|
|
}
|
|
|
|
// Check if character name exists in locked items (could be nested object for field locks or boolean for full lock)
|
|
const charLocks = lockedItems[charName];
|
|
|
|
if (charLocks === true) {
|
|
// Entire character is locked
|
|
// console.log('[Lock Manager] Locking entire character:', charName);
|
|
return { ...char, locked: true };
|
|
} else if (charLocks && typeof charLocks === 'object') {
|
|
// Character has field-level locks
|
|
const modifiedChar = { ...char };
|
|
|
|
for (const fieldName in charLocks) {
|
|
if (charLocks[fieldName] === true) {
|
|
// Check both the original field name and snake_case version
|
|
// (AI returns snake_case, but locks are stored with original configured names)
|
|
// Use the same conversion as toSnakeCase in thoughts.js
|
|
const snakeCaseFieldName = fieldName
|
|
.toLowerCase()
|
|
.replace(/[^\p{L}\p{N}]+/gu, '_')
|
|
.replace(/^_+|_+$/g, '');
|
|
|
|
let locked = false;
|
|
|
|
// Check at root level first (backward compatibility)
|
|
if (modifiedChar[fieldName] !== undefined) {
|
|
// console.log('[Lock Manager] Applying lock to field:', `${charName}.${fieldName}`);
|
|
modifiedChar[fieldName] = {
|
|
value: modifiedChar[fieldName],
|
|
locked: true
|
|
};
|
|
locked = true;
|
|
} else if (modifiedChar[snakeCaseFieldName] !== undefined) {
|
|
// console.log('[Lock Manager] Applying lock to snake_case field:', `${charName}.${snakeCaseFieldName} (from ${fieldName})`);
|
|
modifiedChar[snakeCaseFieldName] = {
|
|
value: modifiedChar[snakeCaseFieldName],
|
|
locked: true
|
|
};
|
|
locked = true;
|
|
}
|
|
|
|
// Check in nested objects (details, relationship, thoughts)
|
|
if (!locked && modifiedChar.details) {
|
|
if (modifiedChar.details[fieldName] !== undefined) {
|
|
// console.log('[Lock Manager] Applying lock to details field:', `${charName}.details.${fieldName}`);
|
|
if (!modifiedChar.details || typeof modifiedChar.details !== 'object') {
|
|
modifiedChar.details = {};
|
|
} else {
|
|
modifiedChar.details = { ...modifiedChar.details };
|
|
}
|
|
modifiedChar.details[fieldName] = {
|
|
value: modifiedChar.details[fieldName],
|
|
locked: true
|
|
};
|
|
locked = true;
|
|
} else if (modifiedChar.details[snakeCaseFieldName] !== undefined) {
|
|
// console.log('[Lock Manager] Applying lock to details snake_case field:', `${charName}.details.${snakeCaseFieldName} (from ${fieldName})`);
|
|
if (!modifiedChar.details || typeof modifiedChar.details !== 'object') {
|
|
modifiedChar.details = {};
|
|
} else {
|
|
modifiedChar.details = { ...modifiedChar.details };
|
|
}
|
|
modifiedChar.details[snakeCaseFieldName] = {
|
|
value: modifiedChar.details[snakeCaseFieldName],
|
|
locked: true
|
|
};
|
|
locked = true;
|
|
}
|
|
}
|
|
|
|
// Check in relationship object
|
|
if (!locked && modifiedChar.relationship) {
|
|
if (modifiedChar.relationship[fieldName] !== undefined) {
|
|
// console.log('[Lock Manager] Applying lock to relationship field:', `${charName}.relationship.${fieldName}`);
|
|
modifiedChar.relationship = { ...modifiedChar.relationship };
|
|
modifiedChar.relationship[fieldName] = {
|
|
value: modifiedChar.relationship[fieldName],
|
|
locked: true
|
|
};
|
|
locked = true;
|
|
} else if (modifiedChar.relationship[snakeCaseFieldName] !== undefined) {
|
|
// console.log('[Lock Manager] Applying lock to relationship snake_case field:', `${charName}.relationship.${snakeCaseFieldName} (from ${fieldName})`);
|
|
modifiedChar.relationship = { ...modifiedChar.relationship };
|
|
modifiedChar.relationship[snakeCaseFieldName] = {
|
|
value: modifiedChar.relationship[snakeCaseFieldName],
|
|
locked: true
|
|
};
|
|
locked = true;
|
|
}
|
|
}
|
|
|
|
// Check in thoughts object
|
|
if (!locked && modifiedChar.thoughts) {
|
|
if (modifiedChar.thoughts[fieldName] !== undefined) {
|
|
// console.log('[Lock Manager] Applying lock to thoughts field:', `${charName}.thoughts.${fieldName}`);
|
|
modifiedChar.thoughts = { ...modifiedChar.thoughts };
|
|
modifiedChar.thoughts[fieldName] = {
|
|
value: modifiedChar.thoughts[fieldName],
|
|
locked: true
|
|
};
|
|
locked = true;
|
|
} else if (modifiedChar.thoughts[snakeCaseFieldName] !== undefined) {
|
|
// console.log('[Lock Manager] Applying lock to thoughts snake_case field:', `${charName}.thoughts.${snakeCaseFieldName} (from ${fieldName})`);
|
|
modifiedChar.thoughts = { ...modifiedChar.thoughts };
|
|
modifiedChar.thoughts[snakeCaseFieldName] = {
|
|
value: modifiedChar.thoughts[snakeCaseFieldName],
|
|
locked: true
|
|
};
|
|
locked = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return modifiedChar;
|
|
}
|
|
|
|
// No locks for this character
|
|
return char;
|
|
});
|
|
|
|
const result = Array.isArray(data)
|
|
? JSON.stringify(characters, null, 2)
|
|
: JSON.stringify({ ...data, characters }, null, 2);
|
|
|
|
// console.log('[Lock Manager] Output data:', result);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Remove locks from tracker data received from AI.
|
|
* Strips "locked": true from all items to clean up the data.
|
|
*
|
|
* @param {string} trackerData - JSON string of tracker data
|
|
* @returns {string} Tracker data with locks removed
|
|
*/
|
|
export function removeLocks(trackerData) {
|
|
if (!trackerData) return trackerData;
|
|
|
|
// Try to parse as JSON
|
|
const parsed = repairJSON(trackerData);
|
|
if (!parsed) {
|
|
// Not JSON format, return as-is
|
|
return trackerData;
|
|
}
|
|
|
|
// Recursively remove all "locked" properties
|
|
const cleaned = removeLockedProperties(parsed);
|
|
|
|
return JSON.stringify(cleaned, null, 2);
|
|
}
|
|
|
|
/**
|
|
* Recursively remove "locked" properties from an object
|
|
* @param {*} obj - Object to clean
|
|
* @returns {*} Object with locked properties removed
|
|
*/
|
|
function removeLockedProperties(obj) {
|
|
if (Array.isArray(obj)) {
|
|
return obj.map(item => removeLockedProperties(item));
|
|
} else if (obj !== null && typeof obj === 'object') {
|
|
const cleaned = {};
|
|
for (const key in obj) {
|
|
if (key !== 'locked') {
|
|
cleaned[key] = removeLockedProperties(obj[key]);
|
|
}
|
|
}
|
|
return cleaned;
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
/**
|
|
* Check if a specific item is locked
|
|
* @param {string} trackerType - Type of tracker
|
|
* @param {string} itemPath - Path to the item (e.g., 'stats.Health', 'quests.main.0')
|
|
* @returns {boolean} Whether the item is locked
|
|
*/
|
|
export function isItemLocked(trackerType, itemPath) {
|
|
const lockedItems = extensionSettings.lockedItems?.[trackerType];
|
|
if (!lockedItems) return false;
|
|
|
|
const parts = itemPath.split('.');
|
|
let current = lockedItems;
|
|
|
|
for (const part of parts) {
|
|
if (current[part] === undefined) return false;
|
|
current = current[part];
|
|
}
|
|
|
|
return !!current;
|
|
}
|
|
|
|
/**
|
|
* Toggle lock state for a specific item
|
|
* @param {string} trackerType - Type of tracker
|
|
* @param {string} itemPath - Path to the item
|
|
* @param {boolean} locked - New lock state
|
|
*/
|
|
export function setItemLock(trackerType, itemPath, locked) {
|
|
// console.log('[Lock Manager] setItemLock called:', { trackerType, itemPath, locked });
|
|
|
|
if (!extensionSettings.lockedItems) {
|
|
extensionSettings.lockedItems = {};
|
|
}
|
|
|
|
if (!extensionSettings.lockedItems[trackerType]) {
|
|
extensionSettings.lockedItems[trackerType] = {};
|
|
}
|
|
|
|
const parts = itemPath.split('.');
|
|
let current = extensionSettings.lockedItems[trackerType];
|
|
|
|
// Navigate to parent of target
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
const part = parts[i];
|
|
if (!current[part]) {
|
|
current[part] = {};
|
|
}
|
|
current = current[part];
|
|
}
|
|
|
|
// Set or remove lock
|
|
const finalKey = parts[parts.length - 1];
|
|
if (locked) {
|
|
current[finalKey] = true;
|
|
} else {
|
|
delete current[finalKey];
|
|
}
|
|
|
|
// console.log('[Lock Manager] Locked items after set:', JSON.stringify(extensionSettings.lockedItems, null, 2));
|
|
}
|