// ==UserScript== // @name DailyAssets Plus // @namespace http://tampermonkey.net/ // @version 1.0.8 // @description 记录每日总资产增长,包含详细统计功能:当前资产、日环比、瞬时时薪、近7天均增速、近7天胜率、近30天日均、最佳/最差日、上一次翻倍等,支持数据导入导出(完全兼容Everyday Profit Pro格式)Record daily total asset growth, including detailed statistics: current assets, daily change, hourly rate, 7-day average growth rate, 7-day win rate, 30-day daily average, best/worst day, last doubling time, etc. Support data import/export (fully compatible with Everyday Profit Pro format). // @author VictoryWinWinWin, PaperCat, Vicky718, SuXingX, ColaCola // @match https://www.milkywayidle.com/* // @match https://www.milkywayidlecn.com/* // @match https://test.milkywayidle.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/544827/DailyAssets%20Plus.user.js // @updateURL https://update.greasyfork.icu/scripts/544827/DailyAssets%20Plus.meta.js // ==/UserScript== (function () { 'use strict'; /* ========================= 语言配置 ========================= */ const LANGUAGES = { zh: { assetsGrowth: '💰总资产增长', historyIcon: '显示详细资产历史图表', metricsIconShow: '显示统计指标', metricsIconHide: '隐藏统计指标', settingsIcon: '图表设置', avg7Days: '近7天日均', lastRecord: '最近记录', chartTitle: '资产历史曲线', timeRange: '时间范围:', settingsTitle: '资产图表设置', viewMode: '视图模式', summaryView: '摘要视图', detailedView: '详细视图', summaryOptions: '摘要视图显示选项', showCurrent: '显示流动资产', showNonCurrent: '显示非流动资产', detailedOptions: '详细视图显示选项', showEquipped: '显示装备价值', showInventory: '显示库存价值', showMarket: '显示订单价值', showHouse: '显示房子价值', showAbility: '显示技能价值', generalOptions: '通用显示选项', showTotal: '显示总资产', unitSettings: '资产显示单位', unitMode: '单位模式:', unitAuto: '自动', unitK: '千(K)', unitM: '百万(M)', unitB: '十亿(B)', timeRangeSettings: '时间范围设置', showTimeRange: '显示时间范围选择器', availableTimeRanges: '可用时间范围:', currentButtons: '当前可用的时间范围按钮:', importExport: '📦 数据导入导出', exportAll: '导出全部(含设置)', exportData: '仅导出数据', importData: '导入数据', preview: '导入预览', version: '版本', exportTime: '导出时间', roleCount: '角色数量', totalRecords: '总记录数', breakdownRecords: '分项资产记录', tagRecords: '标签记录', containsSettings: '包含设置', yes: '是', no: '否', roleStats: '角色数据统计:', records: '条记录', confirmImport: '确认导入', cancel: '取消', exportSuccess: '数据已导出到', exportFailed: '导出失败', importFailed: '导入失败', noImportData: '没有可导入的数据', invalidFormat: '无效的数据格式', unrecognizedVersion: '无法识别的备份文件版本', missingAssetData: '备份文件中缺少资产数据', previewFailed: '预览失败', currentAssets: '当前资产', recentUpdate: '最近更新', dailyChange: '日环比', hourlyRate: '瞬时时薪', hourlyRateDesc: '按当日平均', avg7Growth: '近7天均增速', avg7Desc: '日均增率 / 日均净变动', winRate7: '近7天胜率', avg30Daily: '近30天日均', bestWorstDay: '最佳/最差日', lastDouble: '上一次翻倍', noRecord: '尚无记录', doubling: '翻倍', target: '目标', days: '天', profitLossFlat: '盈/亏/平', equipmentValue: '装备价值', inventoryValue: '库存价值', marketValue: '订单价值', houseValue: '房子价值', abilityValue: '技能价值', currentAssetsValue: '流动资产', nonCurrentAssetsValue: '非流动资产', totalAssets: '总资产', }, en: { assetsGrowth: '💰Total Asset Growth', historyIcon: 'Show asset history chart', metricsIconShow: 'Show statistics', metricsIconHide: 'Hide statistics', settingsIcon: 'Chart settings', avg7Days: '7-day avg', lastRecord: 'Last record', chartTitle: 'Asset History Chart', timeRange: 'Time range:', settingsTitle: 'Asset Chart Settings', viewMode: 'View Mode', summaryView: 'Summary View', detailedView: 'Detailed View', summaryOptions: 'Summary View Options', showCurrent: 'Show Current Assets', showNonCurrent: 'Show Non-Current Assets', detailedOptions: 'Detailed View Options', showEquipped: 'Show Equipment Value', showInventory: 'Show Inventory Value', showMarket: 'Show Market Value', showHouse: 'Show House Value', showAbility: 'Show Ability Value', generalOptions: 'General Options', showTotal: 'Show Total Assets', unitSettings: 'Asset Display Unit', unitMode: 'Unit mode:', unitAuto: 'Auto', unitK: 'K', unitM: 'M', unitB: 'B', timeRangeSettings: 'Time Range Settings', showTimeRange: 'Show time range selector', availableTimeRanges: 'Available time ranges:', currentButtons: 'Current time range buttons:', importExport: '📦 Data Import/Export', exportAll: 'Export All (with settings)', exportData: 'Export Data Only', importData: 'Import Data', preview: 'Import Preview', version: 'Version', exportTime: 'Export time', roleCount: 'Characters', totalRecords: 'Total records', breakdownRecords: 'Breakdown records', tagRecords: 'Tag records', containsSettings: 'Contains settings', yes: 'Yes', no: 'No', roleStats: 'Character statistics:', records: 'records', confirmImport: 'Confirm Import', cancel: 'Cancel', exportSuccess: 'Data exported to', exportFailed: 'Export failed', importFailed: 'Import failed', noImportData: 'No data to import', invalidFormat: 'Invalid data format', unrecognizedVersion: 'Unrecognized backup version', missingAssetData: 'Missing asset data in backup', previewFailed: 'Preview failed', currentAssets: 'Current Assets', recentUpdate: 'Last update', dailyChange: 'Daily Change', hourlyRate: 'Hourly Rate', hourlyRateDesc: 'Based on daily average', avg7Growth: '7-day Avg Growth', avg7Desc: 'Avg rate / Avg change', winRate7: '7-day Win Rate', avg30Daily: '30-day Daily Avg', bestWorstDay: 'Best/Worst Day', lastDouble: 'Last Double', noRecord: 'No record', doubling: 'Double in', target: 'Target', days: 'd', profitLossFlat: 'Win/Loss/Flat', equipmentValue: 'Equipment Value', inventoryValue: 'Inventory Value', marketValue: 'Market Value', houseValue: 'House Value', abilityValue: 'Ability Value', currentAssetsValue: 'Current Assets', nonCurrentAssetsValue: 'Non-Current Assets', totalAssets: 'Total Assets', } }; function getCurrentLanguage() { const lang = GM_getValue('dailyAssetsLanguage', 'zh'); return lang === 'en' ? LANGUAGES.en : LANGUAGES.zh; } function toggleLanguage() { const currentLang = GM_getValue('dailyAssetsLanguage', 'zh'); const newLang = currentLang === 'zh' ? 'en' : 'zh'; GM_setValue('dailyAssetsLanguage', newLang); setTimeout(() => location.reload(), 100); } function t(key) { const lang = getCurrentLanguage(); return lang[key] || key; } /* ========================= 常量与存储键定义 ========================= */ const STORAGE_KEYS = { assetData: 'kbd_asset_data_v2', metricsPrefs: 'kbd_metrics_prefs', metricsPanel: 'kbd_metrics_panel', lastUpdate: 'kbd_last_update_at', }; // Everyday Profit Pro 的存储键 const EP_STORAGE_KEYS = { totalData: 'kbd_calc_data', tags: 'kbd_calc_tags', tagPrefs: 'kbd_calc_tag_prefs', tagPanel: 'kbd_calc_tag_panel', dataPanel: 'kbd_calc_data_panel', lastUpdate: 'kbd_calc_last_update_at', breakdownData: 'kbd_calc_breakdown_data', }; /* ========================= 工具函数 ========================= */ const safeJsonParse = (raw, fallback) => { try { return raw ? JSON.parse(raw) : fallback; } catch { return fallback; } }; const readPrefs = (key) => safeJsonParse(localStorage.getItem(key), {}); const writePrefs = (key, prefs) => localStorage.setItem(key, JSON.stringify(prefs)); const getRoleBoolPref = (key, roleId, defaultValue) => { const prefs = readPrefs(key); if (roleId && Object.prototype.hasOwnProperty.call(prefs, roleId)) return !!prefs[roleId]; return !!defaultValue; }; const setRoleBoolPref = (key, roleId, value) => { if (!roleId) return; const prefs = readPrefs(key); prefs[roleId] = !!value; writePrefs(key, prefs); }; const readRoleLastUpdateMap = () => safeJsonParse(localStorage.getItem(STORAGE_KEYS.lastUpdate), {}); const writeRoleLastUpdateMap = (map) => localStorage.setItem(STORAGE_KEYS.lastUpdate, JSON.stringify(map || {})); const setRoleLastUpdate = (roleId, iso = new Date().toISOString()) => { if (!roleId) return; const map = readRoleLastUpdateMap(); map[roleId] = iso; writeRoleLastUpdateMap(map); }; const getRoleLastUpdate = (roleId) => { if (!roleId) return null; const map = readRoleLastUpdateMap(); return map && map[roleId] ? map[roleId] : null; }; const formatIsoToLocalDateTime = (iso) => { if (!iso) return ''; const d = new Date(iso); if (!Number.isFinite(d.getTime())) return String(iso); const pad = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; }; function parseFormattedNumber(str) { if (!str) return 0; const match = String(str).match(/(-?[\d.,]+)\s*([kKmMbBtT]?)/); if (!match) return 0; let [, numericPart, unit = ''] = match; numericPart = numericPart.replace(/\s+/g, ''); if (!numericPart) return 0; const commaCount = (numericPart.match(/,/g) || []).length; const dotCount = (numericPart.match(/\./g) || []).length; if (commaCount && dotCount) { if (numericPart.lastIndexOf('.') > numericPart.lastIndexOf(',')) { numericPart = numericPart.replace(/,/g, ''); } else { numericPart = numericPart.replace(/\./g, ''); numericPart = numericPart.replace(/,/g, '.'); } } else if (commaCount) { if (commaCount === 1 && numericPart.split(',')[1]?.length <= 2) numericPart = numericPart.replace(',', '.'); else numericPart = numericPart.replace(/,/g, ''); } else if (dotCount > 1) { const parts = numericPart.split('.'); const decimal = parts.pop(); numericPart = parts.join('') + (decimal ? `.${decimal}` : ''); } const num = parseFloat(numericPart); if (isNaN(num)) return 0; const multiplierMap = { k: 1e3, m: 1e6, b: 1e9, t: 1e12 }; const multiplier = multiplierMap[(unit || '').toLowerCase()] || 1; return num * multiplier; } function formatLargeNumber(num, unitMode = 'auto') { const n = Number(num) || 0; const abs = Math.abs(n); switch(unitMode) { case 'k': return (n / 1e3).toFixed(2) + 'K'; case 'm': return (n / 1e6).toFixed(2) + 'M'; case 'b': return (n / 1e9).toFixed(2) + 'B'; case 'auto': default: if (abs >= 1e12) return (n / 1e12).toFixed(2) + 'T'; if (abs >= 1e9) return (n / 1e9).toFixed(2) + 'B'; if (abs >= 1e6) return (n / 1e6).toFixed(2) + 'M'; if (abs >= 1e3) return (n / 1e3).toFixed(2) + 'K'; return n.toFixed(2); } } const formatSignedLargeNumber = (num, unitMode = 'auto') => { const n = Number(num) || 0; return n > 0 ? `+${formatLargeNumber(n, unitMode)}` : formatLargeNumber(n, unitMode); }; function normalizeColor(color) { if (!color) return '#888888'; if (color.length === 4 && color[0] === '#') { return '#' + color[1] + color[1] + color[2] + color[2] + color[3] + color[3]; } return color; } /* ========================= 获取MWITools显示的数据 ========================= */ function getMWIToolsValues() { return new Promise((resolve) => { const checkElement = () => { const netWorthDetails = document.getElementById('netWorthDetails'); if (netWorthDetails && netWorthDetails.children.length > 0) { const values = { equippedNetworth: 0, inventoryNetworth: 0, marketListingsNetworth: 0, totalHouseScore: 0, abilityScore: 0 }; try { if (window.MWITools && window.MWITools.character) { const char = window.MWITools.character; values.equippedNetworth = char.equippedNetworth || 0; values.inventoryNetworth = char.inventoryNetworth || 0; values.marketListingsNetworth = char.marketListingsNetworth || 0; values.totalHouseScore = char.totalHouseScore || 0; values.abilityScore = char.abilityScore || 0; console.log('[DailyAssets] 从 MWITools 读取:', values); resolve(values); return; } const divs = netWorthDetails.querySelectorAll('div'); let foundCount = 0; divs.forEach(div => { const text = div.textContent; if (values.equippedNetworth === 0 && (text.includes('装备价值') || text.includes('Equipment value') || text.includes('Equipped value'))) { const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/); if (match) { values.equippedNetworth = parseFormattedNumber(match[1]); foundCount++; } } else if (values.inventoryNetworth === 0 && (text.includes('库存价值') || text.includes('Inventory value'))) { const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/); if (match) { values.inventoryNetworth = parseFormattedNumber(match[1]); foundCount++; } } else if (values.marketListingsNetworth === 0 && (text.includes('订单价值') || text.includes('Market listing value') || text.includes('Order value'))) { const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/); if (match) { values.marketListingsNetworth = parseFormattedNumber(match[1]); foundCount++; } } else if (values.totalHouseScore === 0 && (text.includes('房子价值') || text.includes('Houses value') || text.includes('House value'))) { const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/); if (match) { values.totalHouseScore = parseFormattedNumber(match[1]); foundCount++; } } else if (values.abilityScore === 0 && (text.includes('技能价值') || text.includes('Abilities value') || text.includes('Skill value'))) { const match = text.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/); if (match) { values.abilityScore = parseFormattedNumber(match[1]); foundCount++; } } }); if (foundCount >= 4) { console.log('[DailyAssets] 从 DOM 读取成功:', values); resolve(values); } else { console.log('[DailyAssets] 未找到足够字段,尝试智能解析'); const currentAssets = document.querySelector('#currentAssets')?.textContent || ''; const nonCurrentAssets = document.querySelector('#nonCurrentAssets')?.textContent || ''; const currentNumbers = currentAssets.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/g) || []; const nonCurrentNumbers = nonCurrentAssets.match(/([+-]?[\d.,]+\s*[kKmMbBtT]?)/g) || []; if (currentNumbers.length >= 3) { values.equippedNetworth = parseFormattedNumber(currentNumbers[0]); values.inventoryNetworth = parseFormattedNumber(currentNumbers[1]); values.marketListingsNetworth = parseFormattedNumber(currentNumbers[2]); } if (nonCurrentNumbers.length >= 2) { values.totalHouseScore = parseFormattedNumber(nonCurrentNumbers[0]); values.abilityScore = parseFormattedNumber(nonCurrentNumbers[1]); } console.log('[DailyAssets] 智能解析结果:', values); resolve(values); } } catch (error) { console.error('[DailyAssets] 读取错误:', error); resolve(values); } } else { setTimeout(checkElement, 1000); } }; checkElement(); }); } /* ========================= 数据存储类 ========================= */ class AssetDataStore { constructor(storageKey = STORAGE_KEYS.assetData, maxDays = 180, currentRole = 'default') { this.storageKey = storageKey; this.maxDays = maxDays; this.currentRole = currentRole; this.data = this.loadFromStorage(); } setRole(roleId) { this.currentRole = roleId; } getRoleData() { if (!this.data[this.currentRole]) { this.data[this.currentRole] = {}; } return this.data[this.currentRole]; } getTodayKey() { const now = new Date(); const utcPlus8 = new Date(now.getTime() + 8 * 3600000); return utcPlus8.toISOString().split('T')[0]; } getYesterdayKey() { const now = new Date(); const yesterday = new Date(now.getTime() - 24 * 3600000); const utcPlus8 = new Date(yesterday.getTime() + 8 * 3600000); return utcPlus8.toISOString().split('T')[0]; } loadFromStorage() { return safeJsonParse(localStorage.getItem(this.storageKey), {}); } saveToStorage() { localStorage.setItem(this.storageKey, JSON.stringify(this.data)); } setTodayDetailedValues(equipped, inventory, market, house, ability) { const roleData = this.getRoleData(); const today = this.getTodayKey(); const totalAssets = equipped + inventory + market + house + ability; const currentAssets = equipped + inventory + market; const nonCurrentAssets = house + ability; roleData[today] = { equippedNetworth: equipped, inventoryNetworth: inventory, marketListingsNetworth: market, totalHouseScore: house, abilityScore: ability, currentAssets: currentAssets, nonCurrentAssets: nonCurrentAssets, totalAssets: totalAssets, timestamp: Date.now() }; this.saveToEPFormat(today, equipped, inventory, market, house, ability, totalAssets); this.cleanupOldData(); this.saveToStorage(); } saveToEPFormat(date, equip, inventory, orders, house, skill, total) { const role = this.currentRole; // 保存总资产到 kbd_calc_data const epTotalData = safeJsonParse(localStorage.getItem(EP_STORAGE_KEYS.totalData), {}); if (!epTotalData[role]) epTotalData[role] = {}; epTotalData[role][date] = total; localStorage.setItem(EP_STORAGE_KEYS.totalData, JSON.stringify(epTotalData)); // 保存分项数据到 kbd_calc_breakdown_data const epBreakdownData = safeJsonParse(localStorage.getItem(EP_STORAGE_KEYS.breakdownData), {}); if (!epBreakdownData[role]) epBreakdownData[role] = {}; epBreakdownData[role][date] = { equip: equip, inventory: inventory, orders: orders, house: house, skill: skill }; localStorage.setItem(EP_STORAGE_KEYS.breakdownData, JSON.stringify(epBreakdownData)); console.log('[DailyAssets] 已保存到 EP 格式:', { role: role, date: date, total: total, breakdown: { equip, inventory, orders, house, skill } }); } cleanupOldData() { const roleData = this.getRoleData(); const keys = Object.keys(roleData).sort(); const cutoff = Date.now() - (this.maxDays * 24 * 3600 * 1000); const newData = {}; keys.forEach(key => { if (roleData[key].timestamp > cutoff) { newData[key] = roleData[key]; } }); this.data[this.currentRole] = newData; } getTodayDeltas() { const roleData = this.getRoleData(); const todayKey = this.getTodayKey(); const yesterdayKey = this.getYesterdayKey(); const todayData = roleData[todayKey] || { equippedNetworth: 0, inventoryNetworth: 0, marketListingsNetworth: 0, totalHouseScore: 0, abilityScore: 0, currentAssets: 0, nonCurrentAssets: 0, totalAssets: 0 }; const yesterdayData = roleData[yesterdayKey] || { equippedNetworth: 0, inventoryNetworth: 0, marketListingsNetworth: 0, totalHouseScore: 0, abilityScore: 0, currentAssets: 0, nonCurrentAssets: 0, totalAssets: 0 }; return { equippedDelta: todayData.equippedNetworth - yesterdayData.equippedNetworth, inventoryDelta: todayData.inventoryNetworth - yesterdayData.inventoryNetworth, marketDelta: todayData.marketListingsNetworth - yesterdayData.marketListingsNetworth, houseDelta: todayData.totalHouseScore - yesterdayData.totalHouseScore, abilityDelta: todayData.abilityScore - yesterdayData.abilityScore, totalDelta: todayData.totalAssets - yesterdayData.totalAssets, totalRatio: yesterdayData.totalAssets > 0 ? (todayData.totalAssets - yesterdayData.totalAssets) / yesterdayData.totalAssets * 100 : 0 }; } getHistoryData(days = 30) { const roleData = this.getRoleData(); const cutoff = Date.now() - (days * 24 * 3600 * 1000); const filtered = Object.entries(roleData) .filter(([_, data]) => data.timestamp > cutoff) .sort(([a], [b]) => new Date(a) - new Date(b)); return { labels: filtered.map(([date]) => date), equippedNetworth: filtered.map(([_, data]) => data.equippedNetworth), inventoryNetworth: filtered.map(([_, data]) => data.inventoryNetworth), marketListingsNetworth: filtered.map(([_, data]) => data.marketListingsNetworth), totalHouseScore: filtered.map(([_, data]) => data.totalHouseScore), abilityScore: filtered.map(([_, data]) => data.abilityScore), currentAssets: filtered.map(([_, data]) => data.currentAssets), nonCurrentAssets: filtered.map(([_, data]) => data.nonCurrentAssets), totalAssets: filtered.map(([_, data]) => data.totalAssets) }; } getHistoryEntriesSorted() { const roleData = this.getRoleData(); return Object.entries(roleData) .filter(([_, data]) => data.totalAssets !== undefined) .map(([date, data]) => [date, data.totalAssets]) .sort(([a], [b]) => new Date(a) - new Date(b)); } getAllRoles() { return Object.keys(this.data); } removeRole(roleId) { delete this.data[roleId]; this.saveToStorage(); } getTotalDataForEP() { const result = {}; Object.entries(this.data).forEach(([role, roleData]) => { result[role] = {}; Object.entries(roleData).forEach(([date, data]) => { result[role][date] = data.totalAssets; }); }); return result; } getBreakdownDataForEP() { const result = {}; Object.entries(this.data).forEach(([role, roleData]) => { result[role] = {}; Object.entries(roleData).forEach(([date, data]) => { result[role][date] = { equip: data.equippedNetworth || 0, inventory: data.inventoryNetworth || 0, orders: data.marketListingsNetworth || 0, house: data.totalHouseScore || 0, skill: data.abilityScore || 0 }; }); }); return result; } debugCurrentData() { console.log('=== DailyAssets Plus 当前数据 ==='); console.log('存储键:', this.storageKey); console.log('当前角色:', this.currentRole); console.log('所有角色:', Object.keys(this.data)); Object.entries(this.data).forEach(([role, roleData]) => { console.log(`\n角色: ${role}`); const dates = Object.keys(roleData).sort(); console.log(`记录天数: ${dates.length}`); if (dates.length > 0) { const latest = dates[dates.length - 1]; const data = roleData[latest]; console.log(`最新记录 (${latest}):`, { 装备: data.equippedNetworth, 库存: data.inventoryNetworth, 订单: data.marketListingsNetworth, 房子: data.totalHouseScore, 技能: data.abilityScore, 总资产: data.totalAssets }); } }); console.log('\n=== EP 格式数据预览 ==='); console.log('kbd_calc_data:', this.getTotalDataForEP()); console.log('kbd_calc_breakdown_data:', this.getBreakdownDataForEP()); } debugEPData() { console.log('=== EP 存储中的数据 ==='); const totalData = safeJsonParse(localStorage.getItem(EP_STORAGE_KEYS.totalData), {}); const breakdownData = safeJsonParse(localStorage.getItem(EP_STORAGE_KEYS.breakdownData), {}); console.log('kbd_calc_data:', totalData); console.log('kbd_calc_breakdown_data:', breakdownData); // 检查当前角色是否有数据 if (this.currentRole) { console.log(`当前角色 ${this.currentRole} 的 EP 数据:`); console.log('总资产:', totalData[this.currentRole]); console.log('分项资产:', breakdownData[this.currentRole]); } } } /* ========================= 统计指标计算函数 ========================= */ const computeDeltas = (sortedEntries) => { const diff = []; for (let i = 1; i < sortedEntries.length; i++) { const prev = sortedEntries[i - 1][1]; const curr = sortedEntries[i][1]; diff.push({ date: sortedEntries[i][0], value: curr - prev, growthPct: prev ? ((curr - prev) / prev) * 100 : 0, }); } return diff; }; const computeStreaks = (differences) => { let bestGain = 0; let worstLoss = 0; let bestDay = null; let worstDay = null; let winStreak = 0; let loseStreak = 0; let currentWin = 0; let currentLose = 0; differences.forEach((d) => { if (d.value >= 0) { currentWin += 1; currentLose = 0; if (d.value > bestGain) { bestGain = d.value; bestDay = d.date; } } else { currentLose += 1; currentWin = 0; if (d.value < worstLoss) { worstLoss = d.value; worstDay = d.date; } } winStreak = Math.max(winStreak, currentWin); loseStreak = Math.max(loseStreak, currentLose); }); return { bestGain, bestDay, worstLoss, worstDay, winStreak, loseStreak }; }; const predictDoublingTime = (differences, currentValue, windowDays = 7) => { if (!currentValue || differences.length === 0) return null; const recent = differences.slice(-windowDays); if (!recent.length) return null; const avgGrowth = recent.reduce((sum, d) => sum + d.value, 0) / recent.length; if (avgGrowth <= 0) return null; return Math.ceil((currentValue) / avgGrowth); }; const predictTargetDate = (differences, currentValue, targetValue) => { if (!currentValue || currentValue >= targetValue) { return { days: 0, targetValue }; } const recent = differences.slice(-7); const avgGrowth = recent.reduce((sum, d) => sum + d.value, 0) / (recent.length || 1); if (avgGrowth <= 0) return null; const remaining = targetValue - currentValue; return { days: Math.ceil(remaining / avgGrowth), targetValue }; }; const nextRoundNumber = (value) => { if (!value) return 0; const magnitude = Math.pow(10, Math.max(3, Math.floor(Math.log10(value)))); return Math.ceil(value / magnitude) * magnitude; }; const computeTotalMetricsFromEntries = (sortedEntries, unitMode = 'auto') => { const latestRecordDate = sortedEntries.length ? sortedEntries[sortedEntries.length - 1][0] : '-'; const valueToPersist = sortedEntries.length ? (sortedEntries[sortedEntries.length - 1][1] || 0) : 0; const differences = computeDeltas(sortedEntries); const todayDelta = differences.length ? differences[differences.length - 1] : null; const growthPct = todayDelta ? (todayDelta.growthPct || 0) : 0; const hourlyRate = todayDelta ? (todayDelta.value / 24) : 0; const last7 = differences.slice(-7); const avgGrowthPct = last7.length ? last7.reduce((sum, d) => sum + (d.growthPct || 0), 0) / last7.length : 0; const avgGrowthValue = last7.length ? last7.reduce((sum, d) => sum + (d.value || 0), 0) / last7.length : 0; const streaks = computeStreaks(differences); const doublingDays = predictDoublingTime(differences, valueToPersist); const targetValue = nextRoundNumber(valueToPersist * 1.05); const targetPrediction = predictTargetDate(differences, valueToPersist, targetValue); let lastDoubleDate = null; let lastDoubleDays = null; let lastDoubleValue = null; if (valueToPersist > 0 && sortedEntries.length) { const halfValue = valueToPersist / 2; const milestoneEntry = sortedEntries.find(([, v]) => Number.isFinite(v) && v >= halfValue); if (milestoneEntry) { lastDoubleDate = milestoneEntry[0]; lastDoubleValue = milestoneEntry[1]; const diffMs = Date.now() - new Date(lastDoubleDate).getTime(); lastDoubleDays = Math.max(0, Math.floor(diffMs / 86400000)); } } return { latestRecordDate, valueToPersist, differences, todayDelta, growthPct, hourlyRate, avgGrowthPct, avgGrowthValue, streaks, lastDoubleDate, lastDoubleDays, lastDoubleValue, doublingDays, targetPrediction, unitMode }; }; const buildMetricCards = (metrics, unitMode = 'auto') => { if (!metrics) return ''; const cards = metrics.map((metric) => `

${metric.title}

${metric.value} ${metric.desc ? `${metric.desc}` : ''}
`).join(''); return `
${cards}
`; }; /* ========================= 设置系统 ========================= */ function getChartOptions() { const defaults = { viewMode: 'summary', summaryShowCurrent: true, summaryShowNonCurrent: true, detailedShowEquipped: true, detailedShowInventory: true, detailedShowMarketListings: true, detailedShowHouse: true, detailedShowAbility: true, showTotal: true, daysToShow: 30, showTimeRangeSettings: true, visibleTimeRanges: [3, 7, 30, 60, 90, 180], unitMode: 'auto', showMetricsPanel: true }; const saved = GM_getValue('chartOptions', defaults); return {...defaults, ...saved}; } function saveChartOptions(options) { GM_setValue('chartOptions', options); } /* ========================= 导入导出管理器(完全兼容 Everyday Profit Pro 格式) ========================= */ class ImportExportManager { constructor(store) { this.store = store; this.importData = null; } // 导出数据(完全兼容 Everyday Profit Pro 格式) exportData(includeSettings = true) { const breakdownData = this.store.getBreakdownDataForEP(); // 确保分项数据只有5个字段 Object.keys(breakdownData).forEach(role => { Object.keys(breakdownData[role] || {}).forEach(date => { const entry = breakdownData[role][date]; breakdownData[role][date] = { equip: entry.equip || 0, inventory: entry.inventory || 0, orders: entry.orders || 0, house: entry.house || 0, skill: entry.skill || 0 }; }); }); const exportObj = { __everyday_profit_backup__: true, schema: 3, exportedAt: new Date().toISOString(), payload: { kbd_calc_data: this.store.getTotalDataForEP(), kbd_calc_breakdown_data: breakdownData, kbd_calc_tags: {}, kbd_calc_tag_prefs: {}, kbd_calc_tag_panel: {}, kbd_calc_data_panel: {}, ep_achievements_data: {} } }; if (includeSettings) { exportObj.settings = { ep_tag_colors: null, ep_window_size: null, ep_chart_settings: GM_getValue('chartOptions', null), ep_heatmap_style: null, ep_glass_heart_mode: null, ep_theme_mode: null, ep_light_bg: null }; } return exportObj; } exportToJson(includeSettings = true) { const exportObj = this.exportData(includeSettings); return JSON.stringify(exportObj, null, 2); } downloadExport(includeSettings = true) { const jsonStr = this.exportToJson(includeSettings); const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const filename = `milkyway_assets_backup_${timestamp}.json`; const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); this.showNotification(`${t('exportSuccess')} ${filename}`, 'success'); } validateImportData(data) { if (!data || typeof data !== 'object') { throw new Error(t('invalidFormat')); } if (data.__everyday_profit_backup__ !== true) { throw new Error(t('unrecognizedVersion')); } if (!data.payload || typeof data.payload !== 'object') { throw new Error(t('missingAssetData')); } if (!data.payload.kbd_calc_data || typeof data.payload.kbd_calc_data !== 'object') { throw new Error(t('missingAssetData')); } return true; } extractFromEverydayProfit(data) { const extracted = { assetData: {} }; if (data.payload && data.payload.kbd_calc_data) { Object.entries(data.payload.kbd_calc_data).forEach(([role, roleData]) => { if (!extracted.assetData[role]) { extracted.assetData[role] = {}; } Object.entries(roleData).forEach(([date, totalValue]) => { extracted.assetData[role][date] = { equippedNetworth: 0, inventoryNetworth: 0, marketListingsNetworth: 0, totalHouseScore: 0, abilityScore: 0, currentAssets: totalValue, nonCurrentAssets: 0, totalAssets: totalValue, timestamp: new Date(date).getTime() }; }); }); } if (data.payload && data.payload.kbd_calc_breakdown_data) { Object.entries(data.payload.kbd_calc_breakdown_data).forEach(([role, roleData]) => { if (!extracted.assetData[role]) { extracted.assetData[role] = {}; } Object.entries(roleData).forEach(([date, values]) => { if (!extracted.assetData[role][date]) { extracted.assetData[role][date] = { timestamp: new Date(date).getTime() }; } const equip = values.equip || 0; const inventory = values.inventory || 0; const orders = values.orders || 0; const house = values.house || 0; const skill = values.skill || 0; const total = equip + inventory + orders + house + skill; extracted.assetData[role][date] = { ...extracted.assetData[role][date], equippedNetworth: equip, inventoryNetworth: inventory, marketListingsNetworth: orders, totalHouseScore: house, abilityScore: skill, currentAssets: equip + inventory + orders, nonCurrentAssets: house + skill, totalAssets: total }; }); }); } return extracted; } previewImport(jsonStr) { try { const data = JSON.parse(jsonStr); this.validateImportData(data); const extracted = this.extractFromEverydayProfit(data); const assetData = extracted.assetData; const roleCount = Object.keys(assetData).length; let totalAssetRecords = 0; Object.values(assetData).forEach(roleData => { totalAssetRecords += roleData ? Object.keys(roleData).length : 0; }); let previewHtml = `
${t('preview')}

📄 ${t('version')}: Everyday Profit Pro

📅 ${t('exportTime')}: ${new Date(data.exportedAt).toLocaleString()}

👥 ${t('roleCount')}: ${roleCount}

💰 ${t('totalRecords')}: ${totalAssetRecords}

`; previewHtml += `

📋 ${t('containsSettings')}: ${data.settings ? t('yes') : t('no')}

`; if (roleCount > 0) { previewHtml += `

📈 ${t('roleStats')}

'; } previewHtml += `
`; this.importData = { data: extracted }; return previewHtml; } catch (error) { throw new Error(`${t('previewFailed')}: ${error.message}`); } } executeImport(options = { merge: true }) { if (!this.importData) { throw new Error(t('noImportData')); } const { data: extracted } = this.importData; // 合并或替换本地数据 if (options.merge) { Object.entries(extracted.assetData).forEach(([role, roleData]) => { if (!this.store.data[role]) { this.store.data[role] = {}; } Object.assign(this.store.data[role], roleData); }); } else { this.store.data = extracted.assetData; } // 保存到本地存储 this.store.saveToStorage(); // 重要:直接写入 Everyday Profit Pro 的存储键 // 从合并后的数据重建 EP 格式 const epTotalData = {}; const epBreakdownData = {}; Object.entries(this.store.data).forEach(([role, roleData]) => { epTotalData[role] = {}; epBreakdownData[role] = {}; Object.entries(roleData).forEach(([date, data]) => { // 写入总资产 epTotalData[role][date] = data.totalAssets; // 写入分项资产 epBreakdownData[role][date] = { equip: data.equippedNetworth || 0, inventory: data.inventoryNetworth || 0, orders: data.marketListingsNetworth || 0, house: data.totalHouseScore || 0, skill: data.abilityScore || 0 }; }); }); // 保存到 localStorage localStorage.setItem(EP_STORAGE_KEYS.totalData, JSON.stringify(epTotalData)); localStorage.setItem(EP_STORAGE_KEYS.breakdownData, JSON.stringify(epBreakdownData)); console.log('[DailyAssets] 导入完成,已写入 EP 存储:', { total: epTotalData, breakdown: epBreakdownData }); this.importData = null; this.showNotification('✅ 导入成功!正在刷新页面...', 'success'); setTimeout(() => location.reload(), 1500); } showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.className = `notification ${type}`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.remove(); }, 3000); } debugCurrentData() { this.store.debugCurrentData(); this.store.debugEPData(); } } function createFileInput() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json,application/json'; input.className = 'file-input-hidden'; input.id = 'importFileInput'; document.body.appendChild(input); return input; } function addImportExportToSettings(settingsModal, importExportManager) { const settingsBody = document.getElementById('assetSettingsBody'); const importExportSection = document.createElement('div'); importExportSection.className = 'import-export-section'; importExportSection.innerHTML = ` ${t('importExport')}
`; settingsBody.appendChild(importExportSection); const fileInput = createFileInput(); document.getElementById('exportFullBtn').addEventListener('click', () => { try { importExportManager.downloadExport(true); } catch (error) { importExportManager.showNotification(`${t('exportFailed')}: ${error.message}`, 'error'); } }); document.getElementById('exportDataBtn').addEventListener('click', () => { try { importExportManager.downloadExport(false); } catch (error) { importExportManager.showNotification(`${t('exportFailed')}: ${error.message}`, 'error'); } }); document.getElementById('importBtn').addEventListener('click', () => { fileInput.click(); }); document.getElementById('debugDataBtn').addEventListener('click', () => { importExportManager.debugCurrentData(); importExportManager.showNotification('数据已输出到控制台 (F12)', 'info'); }); fileInput.addEventListener('change', (event) => { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const previewHtml = importExportManager.previewImport(e.target.result); const previewContainer = document.getElementById('importPreviewContainer'); previewContainer.innerHTML = previewHtml; const confirmBtn = previewContainer.querySelector('.confirm-import-btn'); const cancelBtn = previewContainer.querySelector('.cancel-import-btn'); if (confirmBtn) { confirmBtn.addEventListener('click', () => { try { importExportManager.executeImport({ merge: true }); } catch (error) { importExportManager.showNotification(`${t('importFailed')}: ${error.message}`, 'error'); } }); } if (cancelBtn) { cancelBtn.addEventListener('click', () => { previewContainer.innerHTML = ''; importExportManager.importData = null; fileInput.value = ''; }); } } catch (error) { importExportManager.showNotification(error.message, 'error'); fileInput.value = ''; } }; reader.readAsText(file); }); } GM_addStyle(` /* 基础样式 */ .asset-delta-display { text-align: left; color: #fff; font-size: 16px; margin: 0px 0; } .asset-delta-label { font-weight: bold; margin-right: 5px; } #showHistoryIcon, #showMetricsIcon, #languageToggle { cursor: pointer; margin-left: 8px; font-size: 16px; display: inline-block; margin-top: 0px; opacity: 0.8; transition: opacity 0.2s; } #showHistoryIcon:hover, #showMetricsIcon:hover, #languageToggle:hover { opacity: 1; } #languageToggle { color: #FFD700; } #settingsToggle { cursor: pointer; margin-left: 8px; font-size: 16px; color: #2196F3; opacity: 0.8; transition: opacity 0.2s; } #settingsToggle:hover { opacity: 1; } .positive-delta { color: #4CAF50; font-weight: bold; } .negative-delta { color: #F44336; font-weight: bold; } .neutral-delta { color: #9E9E9E; font-weight: bold; } /* 统计指标面板 */ .ep-metrics-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; padding: 10px 15px; background: #111b2b; border-top: 1px solid rgba(255,255,255,0.05); border-bottom: 1px solid rgba(255,255,255,0.05); } @media (max-width: 720px) { .ep-metrics-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 420px) { .ep-metrics-grid { grid-template-columns: 1fr; } } .ep-metric-card { background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 8px 10px; display: flex; flex-direction: column; gap: 4px; min-width: 0; } .ep-metric-card h4 { font-size: 12px; font-weight: normal; color: #9fb4d1; margin: 0; } .ep-metric-card strong { font-size: 18px; color: #f7fafc; word-break: break-word; } .ep-metric-card span { font-size: 12px; color: #7f8ca3; word-break: break-word; } /* 额外信息行 */ .ep-delta-extra { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; font-size: 14px; color: #cfd8e3; } .ep-delta-extra span { background: rgba(255, 255, 255, 0.08); border-radius: 4px; padding: 3px 8px; } /* 弹窗样式 */ #deltaNetworthChartModal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 1200px; max-width: 95vw; background: #1e1e1e; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.6); z-index: 10000; display: none; flex-direction: column; color: #f5f5f5; border: 1px solid rgba(255,255,255,0.08); } #deltaNetworthChartModal.dragging { cursor: grabbing; } #deltaNetworthChartHeader { padding: 10px 15px; background: #333; color: white; font-weight: bold; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; border-top-left-radius: 8px; border-top-right-radius: 8px; } #netWorthChartBody { padding: 15px; background: #0b1522; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; border: 1px solid rgba(255,255,255,0.05); } #netWorthChart { width: 100%; height: 400px; background: radial-gradient(circle at top, rgba(0,198,255,0.08), rgba(2,12,24,0.95)); border-radius: 6px; } /* 设置弹窗样式 */ #assetSettingsModal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 600px; max-width: 95vw; background: #1e1e1e; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.6); z-index: 10001; display: none; flex-direction: column; } #assetSettingsModal.dragging { cursor: grabbing; } #assetSettingsHeader { padding: 10px 15px; background: #333; color: white; font-weight: bold; display: flex; justify-content: space-between; align-items: center; cursor: default; user-select: none; border-top-left-radius: 8px; border-top-right-radius: 8px; } #assetSettingsBody { padding: 15px; max-height: 70vh; overflow-y: auto; } /* 模态遮罩 */ .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 9999; display: none; } /* 关闭按钮 */ .close-btn { cursor: pointer; font-size: 18px; color: #fff; } .close-btn:hover { color: #f44336; } /* 视图切换 */ .view-toggle { display: flex; background: #333; border-radius: 4px; padding: 2px; margin-bottom: 15px; } .view-option { flex: 1; text-align: center; padding: 8px; cursor: pointer; border-radius: 3px; font-weight: bold; transition: all 0.3s ease; } .view-option.active { background: #4CAF50; color: white; } .view-option:not(.active) { background: transparent; color: #ccc; } .view-option:not(.active):hover { background: #444; } /* 设置区域 */ .settings-section { background: #2a2a2a; padding: 15px; margin: 2px 0; border-radius: 4px; } .settings-title { color: #fff; font-weight: bold; font-size: 16px; margin-bottom: 5px; display: block; } .settings-group { color: #fff; margin-bottom: 15px; } .chart-option { margin: 5px 0; display: flex; align-items: center; } .chart-option input { margin-right: 8px; } .chart-option label { cursor: pointer; color: white; flex-grow: 1; } /* 时间范围按钮 */ .time-range-btn { padding: 5px 10px; background: rgba(0,0,0,.3); color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; margin-bottom: 5px; } .time-range-btn:hover { background: #555; } .time-range-btn.active { background: rgb(25, 118, 210); font-weight: bold; } .time-range-section { margin: 15px 0; } /* 图表选项容器 */ #chartOptionsContainer { padding: 4px; background: #111b2b; border-bottom: 0px solid #333; } #timeRangeOptions { margin-top: 10px; color: #fff; } #timeRangeOptions.hidden { display: none; } .time-range-buttons { margin-top: 10px; } /* 单位设置 */ .unit-toggle { display: flex; background: #333; border-radius: 4px; padding: 2px; margin-top: 8px; } .unit-option { flex: 1; text-align: center; padding: 6px; cursor: pointer; border-radius: 3px; font-weight: bold; font-size: 14px; transition: all 0.3s ease; } .unit-option.active { background: #2196F3; color: white; } .unit-option:not(.active) { background: transparent; color: #ccc; } .unit-option:not(.active):hover { background: #444; } /* 导入导出样式 */ .import-export-section { background: #2a2a2a; padding: 15px; margin: 2px 0; border-radius: 4px; } .import-export-buttons { display: flex; gap: 10px; margin-top: 10px; flex-wrap: wrap; } .import-export-btn { padding: 8px 15px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.2s; flex: 1; min-width: 100px; } .import-export-btn:hover { background: #1976D2; } .import-export-btn.export-all { background: #4CAF50; } .import-export-btn.export-all:hover { background: #45a049; } .import-export-btn.import { background: #FF9800; } .import-export-btn.import:hover { background: #F57C00; } .file-input-hidden { display: none; } .import-preview { margin-top: 15px; padding: 10px; background: #333; border-radius: 4px; display: none; } .import-preview.show { display: block; } .import-preview-title { color: #fff; font-weight: bold; margin-bottom: 10px; } .import-preview-content { max-height: 200px; overflow-y: auto; color: #ccc; font-size: 12px; } .import-preview-actions { display: flex; gap: 10px; margin-top: 10px; } .confirm-import-btn { background: #4CAF50; color: white; border: none; border-radius: 4px; padding: 5px 10px; cursor: pointer; } .cancel-import-btn { background: #f44336; color: white; border: none; border-radius: 4px; padding: 5px 10px; cursor: pointer; } .notification { position: fixed; top: 20px; right: 20px; padding: 10px 20px; background: #333; color: white; border-radius: 4px; z-index: 10002; animation: slideIn 0.3s ease; } .notification.success { background: #4CAF50; } .notification.error { background: #f44336; } .notification.info { background: #2196F3; } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } `); /* ========================= 主逻辑 ========================= */ window.kbd_calculateTotalNetworth = function kbd_calculateTotalNetworth( equippedNetworth, inventoryNetworth, marketListingsNetworth, totalHouseScore, abilityScore, dom ) { const detectRoleId = () => { const candidates = [ document.querySelector('.CharacterName_name__1amXp span'), document.querySelector('[class*="CharacterName_name"] span'), document.querySelector('[data-testid="character-name"]'), ]; const text = (candidates.find(Boolean)?.textContent || '').replace(/\s+/g, ' ').trim(); return text || 'default'; }; const roleId = detectRoleId(); console.log('[DailyAssets] 检测到角色ID:', roleId); const store = new AssetDataStore(); store.setRole(roleId); // 确保调试对象正确初始化 window.__dailyAssetsDebug = { store: store, debug: () => store.debugCurrentData(), debugEP: () => store.debugEPData(), checkEP: () => { console.log('=== 检查 EP 存储 ==='); const total = localStorage.getItem('kbd_calc_data'); const breakdown = localStorage.getItem('kbd_calc_breakdown_data'); console.log('kbd_calc_data 是否存在:', !!total); console.log('kbd_calc_breakdown_data 是否存在:', !!breakdown); if (total) { console.log('kbd_calc_data 内容:', JSON.parse(total)); } if (breakdown) { console.log('kbd_calc_breakdown_data 内容:', JSON.parse(breakdown)); } } }; console.log('[DailyAssets] 调试对象已初始化:', window.__dailyAssetsDebug); let chart = null; let currentModal = null; let metricsPanelVisible = getRoleBoolPref(STORAGE_KEYS.metricsPanel, roleId, true); function createOverlay() { const overlay = document.createElement('div'); overlay.className = 'modal-overlay'; overlay.id = 'modalOverlay'; overlay.addEventListener('click', (e) => { if (e.target === overlay) { hideAllModals(); } }); document.body.appendChild(overlay); return overlay; } function showOverlay() { const overlay = document.getElementById('modalOverlay') || createOverlay(); overlay.style.display = 'block'; } function hideOverlay() { const overlay = document.getElementById('modalOverlay'); if (overlay) { overlay.style.display = 'none'; } } function hideAllModals() { document.querySelectorAll('#deltaNetworthChartModal, #assetSettingsModal').forEach(modal => { modal.style.display = 'none'; }); hideOverlay(); currentModal = null; } function setupDrag(modal) { let isDragging = false; let startX, startY, initialLeft, initialTop; const header = modal.querySelector('#deltaNetworthChartHeader') || modal.querySelector('#assetSettingsHeader'); header.addEventListener('mousedown', (e) => { if (e.target.className === 'close-btn') return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = modal.getBoundingClientRect(); initialLeft = rect.left; initialTop = rect.top; modal.classList.add('dragging'); e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (isDragging) { const dx = e.clientX - startX; const dy = e.clientY - startY; modal.style.left = `${initialLeft + dx}px`; modal.style.top = `${initialTop + dy}px`; modal.style.transform = 'none'; } }); document.addEventListener('mouseup', () => { isDragging = false; modal.classList.remove('dragging'); }); } function renderMetricsPanel() { const entries = store.getHistoryEntriesSorted(); if (entries.length < 2) return ''; const options = getChartOptions(); const mtxAll = computeTotalMetricsFromEntries(entries, options.unitMode); const lastUpdateIso = getRoleLastUpdate(roleId); const lastUpdateStr = lastUpdateIso ? formatIsoToLocalDateTime(lastUpdateIso) : (mtxAll.latestRecordDate || '-'); const diffs = Array.isArray(mtxAll.differences) ? mtxAll.differences : []; const last7Diffs = diffs.slice(-7); const last30Diffs = diffs.slice(-30); const winCount7 = last7Diffs.filter((d) => d && Number.isFinite(d.value) && d.value > 0).length; const loseCount7 = last7Diffs.filter((d) => d && Number.isFinite(d.value) && d.value < 0).length; const flatCount7 = last7Diffs.filter((d) => d && Number.isFinite(d.value) && d.value === 0).length; const rate7 = last7Diffs.length ? (winCount7 / last7Diffs.length) * 100 : null; const avg30Value = last30Diffs.length ? last30Diffs.reduce((sum, d) => sum + (d.value || 0), 0) / last30Diffs.length : null; const avg30Pct = last30Diffs.length ? last30Diffs.reduce((sum, d) => sum + (d.growthPct || 0), 0) / last30Diffs.length : null; const bestWorstValue = `${formatSignedLargeNumber(mtxAll.streaks?.bestGain || 0, options.unitMode)} / ${formatSignedLargeNumber(mtxAll.streaks?.worstLoss || 0, options.unitMode)}`; const bestWorstDesc = `${mtxAll.streaks?.bestDay || '-'} | ${mtxAll.streaks?.worstDay || '-'}`; const predictBits = []; if (mtxAll.doublingDays) predictBits.push(`${t('doubling')}: ${mtxAll.doublingDays}${t('days')}`); if (mtxAll.targetPrediction) predictBits.push(`${t('target')}: ${mtxAll.targetPrediction.days}${t('days')}→${formatLargeNumber(mtxAll.targetPrediction.targetValue, options.unitMode)}`); const metrics = [ { title: t('currentAssets'), value: formatLargeNumber(mtxAll.valueToPersist || 0, options.unitMode), desc: `${t('recentUpdate')}: ${lastUpdateStr}` }, { title: t('dailyChange'), value: `${(mtxAll.growthPct >= 0 ? '+' : '')}${(mtxAll.growthPct || 0).toFixed(2)}%`, desc: formatSignedLargeNumber(mtxAll.todayDelta?.value || 0, options.unitMode), }, { title: t('hourlyRate'), value: formatLargeNumber(mtxAll.hourlyRate || 0, options.unitMode), desc: t('hourlyRateDesc') }, { title: t('avg7Growth'), value: `${(mtxAll.avgGrowthPct >= 0 ? '+' : '')}${(mtxAll.avgGrowthPct || 0).toFixed(2)}% / ${formatSignedLargeNumber(mtxAll.avgGrowthValue || 0, options.unitMode)}`, desc: t('avg7Desc'), }, { title: t('winRate7'), value: rate7 === null ? '—' : `${winCount7}/${last7Diffs.length} (${rate7.toFixed(0)}%)`, desc: `${t('profitLossFlat')}: ${winCount7}/${loseCount7}/${flatCount7}`, }, { title: t('avg30Daily'), value: avg30Value === null ? '—' : `${(avg30Pct >= 0 ? '+' : '')}${(avg30Pct || 0).toFixed(2)}% / ${formatSignedLargeNumber(avg30Value || 0, options.unitMode)}`, desc: predictBits.length ? `${predictBits.join(' | ')}` : t('avg7Desc'), }, { title: t('bestWorstDay'), value: bestWorstValue, desc: bestWorstDesc }, { title: t('lastDouble'), value: Number.isFinite(mtxAll.lastDoubleDays) ? `${mtxAll.lastDoubleDays} ${t('days')}` : t('noRecord'), desc: mtxAll.lastDoubleDate ? `${mtxAll.lastDoubleDate}: ${formatLargeNumber(mtxAll.lastDoubleValue || 0, options.unitMode)}` : '—', }, ]; return buildMetricCards(metrics); } const updateDisplay = (isFirst = false) => { console.log('[DailyAssets] 更新数据:', { equipped: equippedNetworth, inventory: inventoryNetworth, market: marketListingsNetworth, house: totalHouseScore, ability: abilityScore }); store.setTodayDetailedValues( equippedNetworth, inventoryNetworth, marketListingsNetworth, totalHouseScore, abilityScore ); setRoleLastUpdate(roleId); const deltas = store.getTodayDeltas(); const options = getChartOptions(); const formattedTotalDelta = formatLargeNumber(deltas.totalDelta, options.unitMode); const totalDeltaClass = deltas.totalDelta > 0 ? 'positive-delta' : (deltas.totalDelta < 0 ? 'negative-delta' : 'neutral-delta'); const entries = store.getHistoryEntriesSorted(); const last7 = entries.slice(-8); let avg7 = 0; if (last7.length >= 2) { let s = 0; let c = 0; for (let i = 1; i < last7.length; i++) { const d = last7[i][1] - last7[i - 1][1]; if (Number.isFinite(d)) { s += d; c += 1; } } avg7 = c ? s / c : 0; } const lastUpdateIso = getRoleLastUpdate(roleId); const lastUpdateStr = lastUpdateIso ? formatIsoToLocalDateTime(lastUpdateIso) : (entries.length ? entries[entries.length - 1][0] : '—'); if (isFirst) { const metricsHTML = metricsPanelVisible ? renderMetricsPanel() : ''; dom.insertAdjacentHTML('afterend', `
${t('assetsGrowth')}: ${formattedTotalDelta} 📊 📈 🌐 ⚙️
${t('avg7Days')}: ${formatSignedLargeNumber(avg7, options.unitMode)} ${t('lastRecord')}: ${lastUpdateStr}
${metricsHTML}
`); const chartModal = document.createElement('div'); chartModal.id = 'deltaNetworthChartModal'; chartModal.innerHTML = `
${t('chartTitle')} (v${GM_info.script.version})
${t('timeRange')}
`; document.body.appendChild(chartModal); const settingsModal = document.createElement('div'); settingsModal.id = 'assetSettingsModal'; settingsModal.innerHTML = `
${t('settingsTitle')} (v${GM_info.script.version})
${t('viewMode')}
${t('summaryView')}
${t('detailedView')}
${t('summaryOptions')}
${t('generalOptions')}
${t('unitSettings')}
${t('unitMode')}
${t('unitAuto')}
${t('unitK')}
${t('unitM')}
${t('unitB')}
${t('timeRangeSettings')}
${t('availableTimeRanges')}
${t('currentButtons')}
`; document.body.appendChild(settingsModal); initSettings(); const importExportManager = new ImportExportManager(store); addImportExportToSettings(settingsModal, importExportManager); window.__dailyAssetsImportExport = importExportManager; document.getElementById('showHistoryIcon').addEventListener('click', () => showChartModal()); document.getElementById('settingsToggle').addEventListener('click', () => showSettingsModal()); document.getElementById('showMetricsIcon').addEventListener('click', toggleMetricsPanel); document.getElementById('languageToggle').addEventListener('click', toggleLanguage); document.getElementById('chartModalCloseBtn').addEventListener('click', hideAllModals); document.getElementById('settingsModalCloseBtn').addEventListener('click', hideAllModals); document.querySelectorAll('.view-option').forEach(option => { option.addEventListener('click', (e) => { const view = e.target.dataset.view; switchView(view); }); }); document.querySelectorAll('.unit-option').forEach(option => { option.addEventListener('click', (e) => { const unit = e.target.dataset.unit; switchUnit(unit); }); }); document.getElementById('summaryShowCurrent').addEventListener('change', updateChartVisibility); document.getElementById('summaryShowNonCurrent').addEventListener('change', updateChartVisibility); document.getElementById('detailedShowEquipped').addEventListener('change', updateChartVisibility); document.getElementById('detailedShowInventory').addEventListener('change', updateChartVisibility); document.getElementById('detailedShowMarketListings').addEventListener('change', updateChartVisibility); document.getElementById('detailedShowHouse').addEventListener('change', updateChartVisibility); document.getElementById('detailedShowAbility').addEventListener('change', updateChartVisibility); document.getElementById('showTotalOption').addEventListener('change', updateChartVisibility); document.getElementById('showTimeRangeToggle').addEventListener('change', toggleTimeRangeVisibility); document.getElementById('timeRange3').addEventListener('change', updateTimeRangeSettings); document.getElementById('timeRange7').addEventListener('change', updateTimeRangeSettings); document.getElementById('timeRange30').addEventListener('change', updateTimeRangeSettings); document.getElementById('timeRange60').addEventListener('change', updateTimeRangeSettings); document.getElementById('timeRange90').addEventListener('change', updateTimeRangeSettings); document.getElementById('timeRange180').addEventListener('change', updateTimeRangeSettings); updateSettingsTimeRangeButtons(); setupDrag(chartModal); setupDrag(settingsModal); } else { const container = document.getElementById('assetDeltaContainer'); if (container) { const metricsHTML = metricsPanelVisible ? renderMetricsPanel() : ''; container.innerHTML = `
${t('assetsGrowth')}: ${formattedTotalDelta} 📊 📈 🌐 ⚙️
${t('avg7Days')}: ${formatSignedLargeNumber(avg7, options.unitMode)} ${t('lastRecord')}: ${lastUpdateStr}
${metricsHTML} `; document.getElementById('showHistoryIcon').addEventListener('click', () => showChartModal()); document.getElementById('settingsToggle').addEventListener('click', () => showSettingsModal()); document.getElementById('showMetricsIcon').addEventListener('click', toggleMetricsPanel); document.getElementById('languageToggle').addEventListener('click', toggleLanguage); } } }; function toggleMetricsPanel() { metricsPanelVisible = !metricsPanelVisible; setRoleBoolPref(STORAGE_KEYS.metricsPanel, roleId, metricsPanelVisible); updateDisplay(); } function switchView(viewMode) { const options = getChartOptions(); options.viewMode = viewMode; saveChartOptions(options); document.querySelectorAll('.view-option').forEach(option => { if (option.dataset.view === viewMode) { option.classList.add('active'); } else { option.classList.remove('active'); } }); const summaryOptions = document.querySelector('.summary-view-options'); const detailedOptions = document.querySelector('.detailed-view-options'); if (viewMode === 'summary') { summaryOptions.classList.remove('hidden'); detailedOptions.classList.add('hidden'); } else { summaryOptions.classList.add('hidden'); detailedOptions.classList.remove('hidden'); } if (chart) { recreateChart(); } } function switchUnit(unitMode) { const options = getChartOptions(); options.unitMode = unitMode; saveChartOptions(options); document.querySelectorAll('.unit-option').forEach(option => { if (option.dataset.unit === unitMode) { option.classList.add('active'); } else { option.classList.remove('active'); } }); updateDisplay(); if (chart) { chart.destroy(); chart = null; initializeChart(); } } function initSettings() { const options = getChartOptions(); switchView(options.viewMode); document.getElementById('summaryShowCurrent').checked = options.summaryShowCurrent; document.getElementById('summaryShowNonCurrent').checked = options.summaryShowNonCurrent; document.getElementById('detailedShowEquipped').checked = options.detailedShowEquipped; document.getElementById('detailedShowInventory').checked = options.detailedShowInventory; document.getElementById('detailedShowMarketListings').checked = options.detailedShowMarketListings; document.getElementById('detailedShowHouse').checked = options.detailedShowHouse; document.getElementById('detailedShowAbility').checked = options.detailedShowAbility; document.getElementById('showTotalOption').checked = options.showTotal; switchUnit(options.unitMode); document.getElementById('showTimeRangeToggle').checked = options.showTimeRangeSettings; document.getElementById('timeRange3').checked = options.visibleTimeRanges.includes(3); document.getElementById('timeRange7').checked = options.visibleTimeRanges.includes(7); document.getElementById('timeRange30').checked = options.visibleTimeRanges.includes(30); document.getElementById('timeRange60').checked = options.visibleTimeRanges.includes(60); document.getElementById('timeRange90').checked = options.visibleTimeRanges.includes(90); document.getElementById('timeRange180').checked = options.visibleTimeRanges.includes(180); const timeRangeOptions = document.getElementById('timeRangeOptions'); if (timeRangeOptions) { if (options.showTimeRangeSettings) { timeRangeOptions.classList.remove('hidden'); } else { timeRangeOptions.classList.add('hidden'); } } updateSettingsTimeRangeButtons(); } function recreateChart() { if (chart) { chart.destroy(); chart = null; } initializeChart(); } function showChartModal() { hideAllModals(); currentModal = document.getElementById('deltaNetworthChartModal'); showOverlay(); currentModal.style.display = 'flex'; const metricsContainer = document.getElementById('ep-metrics-container'); if (metricsContainer) { metricsContainer.innerHTML = renderMetricsPanel(); } if (!window.Chart) { loadChartLibrary().then(() => { initializeChart(); updateChartTimeRangeButtons(); }); } else if (!chart) { initializeChart(); updateChartTimeRangeButtons(); } else { updateChart(); updateChartTimeRangeButtons(); } } function showSettingsModal() { hideAllModals(); currentModal = document.getElementById('assetSettingsModal'); showOverlay(); currentModal.style.display = 'flex'; updateSettingsTimeRangeButtons(); } function loadChartLibrary() { return new Promise((resolve) => { if (window.Chart) { resolve(); return; } const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js'; script.onload = resolve; document.head.appendChild(script); }); } function initializeChart() { const options = getChartOptions(); const historyData = store.getHistoryData(options.daysToShow); const ctx = document.getElementById('netWorthChart').getContext('2d'); const datasets = []; if (options.viewMode === 'detailed') { datasets.push({ id: 'equipped', label: t('equipmentValue'), data: historyData.equippedNetworth, borderColor: 'rgba(75, 192, 192, 1)', backgroundColor: 'rgba(75, 192, 192, 0.1)', tension: 0.3, fill: false, hidden: !options.detailedShowEquipped }); datasets.push({ id: 'inventory', label: t('inventoryValue'), data: historyData.inventoryNetworth, borderColor: 'rgba(255, 159, 64, 1)', backgroundColor: 'rgba(255, 159, 64, 0.1)', tension: 0.3, fill: false, hidden: !options.detailedShowInventory }); datasets.push({ id: 'market', label: t('marketValue'), data: historyData.marketListingsNetworth, borderColor: 'rgba(153, 102, 255, 1)', backgroundColor: 'rgba(153, 102, 255, 0.1)', tension: 0.3, fill: false, hidden: !options.detailedShowMarketListings }); datasets.push({ id: 'house', label: t('houseValue'), data: historyData.totalHouseScore, borderColor: 'rgba(255, 99, 132, 1)', backgroundColor: 'rgba(255, 99, 132, 0.1)', tension: 0.3, fill: false, hidden: !options.detailedShowHouse }); datasets.push({ id: 'ability', label: t('abilityValue'), data: historyData.abilityScore, borderColor: 'rgba(54, 162, 235, 1)', backgroundColor: 'rgba(54, 162, 235, 0.1)', tension: 0.3, fill: false, hidden: !options.detailedShowAbility }); } else { datasets.push({ id: 'current', label: t('currentAssetsValue'), data: historyData.currentAssets, borderColor: 'rgba(75, 192, 192, 1)', backgroundColor: 'rgba(75, 192, 192, 0.1)', tension: 0.3, fill: false, hidden: !options.summaryShowCurrent }); datasets.push({ id: 'nonCurrent', label: t('nonCurrentAssetsValue'), data: historyData.nonCurrentAssets, borderColor: 'rgba(255, 99, 132, 1)', backgroundColor: 'rgba(255, 99, 132, 0.1)', tension: 0.3, fill: false, hidden: !options.summaryShowNonCurrent }); } datasets.push({ id: 'total', label: t('totalAssets'), data: historyData.totalAssets, borderColor: 'rgba(54, 162, 235, 1)', backgroundColor: 'rgba(54, 162, 235, 0.1)', tension: 0.3, fill: false, hidden: !options.showTotal, borderWidth: 2 }); chart = new Chart(ctx, { type: 'line', data: { labels: historyData.labels, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'top', labels: { usePointStyle: true, boxWidth: 10, color: '#fff' } }, tooltip: { callbacks: { label: (context) => { const label = context.dataset.label || ''; const value = formatLargeNumber(context.raw, options.unitMode); return `${label}: ${value}`; } }, backgroundColor: 'rgba(0, 0, 0, 0.8)', titleColor: '#fff', bodyColor: '#fff' } }, scales: { x: { ticks: { color: '#ccc' }, grid: { color: 'rgba(255, 255, 255, 0.1)' } }, y: { ticks: { color: '#ccc', callback: (value) => formatLargeNumber(value, options.unitMode) }, grid: { color: 'rgba(255, 255, 255, 0.1)' } } } } }); } function updateChart() { const options = getChartOptions(); const historyData = store.getHistoryData(options.daysToShow); chart.data.labels = historyData.labels; if (options.viewMode === 'detailed') { if (chart.data.datasets.length >= 6) { chart.data.datasets[0].data = historyData.equippedNetworth; chart.data.datasets[1].data = historyData.inventoryNetworth; chart.data.datasets[2].data = historyData.marketListingsNetworth; chart.data.datasets[3].data = historyData.totalHouseScore; chart.data.datasets[4].data = historyData.abilityScore; } } else { if (chart.data.datasets.length >= 3) { chart.data.datasets[0].data = historyData.currentAssets; chart.data.datasets[1].data = historyData.nonCurrentAssets; } } const totalIndex = options.viewMode === 'detailed' ? 5 : 2; if (chart.data.datasets[totalIndex]) { chart.data.datasets[totalIndex].data = historyData.totalAssets; } chart.options.scales.y.ticks.callback = (value) => formatLargeNumber(value, options.unitMode); chart.options.plugins.tooltip.callbacks.label = (context) => { const label = context.dataset.label || ''; const value = formatLargeNumber(context.raw, options.unitMode); return `${label}: ${value}`; }; chart.update(); } function updateChartVisibility() { const options = getChartOptions(); options.summaryShowCurrent = document.getElementById('summaryShowCurrent').checked; options.summaryShowNonCurrent = document.getElementById('summaryShowNonCurrent').checked; options.detailedShowEquipped = document.getElementById('detailedShowEquipped').checked; options.detailedShowInventory = document.getElementById('detailedShowInventory').checked; options.detailedShowMarketListings = document.getElementById('detailedShowMarketListings').checked; options.detailedShowHouse = document.getElementById('detailedShowHouse').checked; options.detailedShowAbility = document.getElementById('detailedShowAbility').checked; options.showTotal = document.getElementById('showTotalOption').checked; saveChartOptions(options); if (chart) { if (options.viewMode === 'detailed') { if (chart.data.datasets.length >= 6) { chart.data.datasets[0].hidden = !options.detailedShowEquipped; chart.data.datasets[1].hidden = !options.detailedShowInventory; chart.data.datasets[2].hidden = !options.detailedShowMarketListings; chart.data.datasets[3].hidden = !options.detailedShowHouse; chart.data.datasets[4].hidden = !options.detailedShowAbility; chart.data.datasets[5].hidden = !options.showTotal; } } else { if (chart.data.datasets.length >= 3) { chart.data.datasets[0].hidden = !options.summaryShowCurrent; chart.data.datasets[1].hidden = !options.summaryShowNonCurrent; chart.data.datasets[2].hidden = !options.showTotal; } } chart.update(); } } function toggleTimeRangeVisibility() { const show = document.getElementById('showTimeRangeToggle').checked; const timeRangeOptions = document.getElementById('timeRangeOptions'); const options = getChartOptions(); options.showTimeRangeSettings = show; saveChartOptions(options); if (show) { timeRangeOptions.classList.remove('hidden'); } else { timeRangeOptions.classList.add('hidden'); } } function updateTimeRangeSettings() { const options = getChartOptions(); const visibleRanges = []; if (document.getElementById('timeRange3').checked) visibleRanges.push(3); if (document.getElementById('timeRange7').checked) visibleRanges.push(7); if (document.getElementById('timeRange30').checked) visibleRanges.push(30); if (document.getElementById('timeRange60').checked) visibleRanges.push(60); if (document.getElementById('timeRange90').checked) visibleRanges.push(90); if (document.getElementById('timeRange180').checked) visibleRanges.push(180); options.visibleTimeRanges = visibleRanges; saveChartOptions(options); updateChartTimeRangeButtons(); updateSettingsTimeRangeButtons(); } function updateChartTimeRangeButtons() { const timeRangeOptions = document.getElementById('timeRangeOptions'); const options = getChartOptions(); const titleSpan = timeRangeOptions.querySelector('span'); timeRangeOptions.innerHTML = ''; if (titleSpan) { timeRangeOptions.appendChild(titleSpan); } options.visibleTimeRanges.forEach(days => { const btn = document.createElement('button'); btn.id = `btn${days}Days`; btn.className = 'time-range-btn'; if (options.daysToShow === days) { btn.classList.add('active'); } btn.textContent = `${days}${t('days')}`; btn.addEventListener('click', () => updateChartTimeRange(days)); timeRangeOptions.appendChild(btn); }); } function updateSettingsTimeRangeButtons() { const buttonsContainer = document.getElementById('settingsTimeRangeButtons'); const options = getChartOptions(); if (!buttonsContainer) return; buttonsContainer.innerHTML = ''; options.visibleTimeRanges.forEach(days => { const btn = document.createElement('button'); btn.id = `settingsBtn${days}Days`; btn.className = 'time-range-btn'; if (options.daysToShow === days) { btn.classList.add('active'); } btn.textContent = `${days}${t('days')}`; btn.addEventListener('click', () => { updateChartTimeRange(days); updateSettingsTimeRangeButtons(); updateChartTimeRangeButtons(); }); buttonsContainer.appendChild(btn); }); } function updateChartTimeRange(days) { const options = getChartOptions(); options.daysToShow = days; saveChartOptions(options); updateChartTimeRangeButtons(); updateSettingsTimeRangeButtons(); if (chart) { const historyData = store.getHistoryData(days); chart.data.labels = historyData.labels; if (options.viewMode === 'detailed') { if (chart.data.datasets.length >= 6) { chart.data.datasets[0].data = historyData.equippedNetworth; chart.data.datasets[1].data = historyData.inventoryNetworth; chart.data.datasets[2].data = historyData.marketListingsNetworth; chart.data.datasets[3].data = historyData.totalHouseScore; chart.data.datasets[4].data = historyData.abilityScore; } } else { if (chart.data.datasets.length >= 3) { chart.data.datasets[0].data = historyData.currentAssets; chart.data.datasets[1].data = historyData.nonCurrentAssets; } } const totalIndex = options.viewMode === 'detailed' ? 5 : 2; if (chart.data.datasets[totalIndex]) { chart.data.datasets[totalIndex].data = historyData.totalAssets; } chart.update(); } } updateDisplay(true); setInterval(() => updateDisplay(false), 10 * 60 * 1000); }; /* ========================= 页面检测与执行 ========================= */ const checkAssetsAndRun = async () => { console.log('[DailyAssets] 开始检查资产数据...'); const mwValues = await getMWIToolsValues(); console.log('[DailyAssets] 最终读取结果:', mwValues); const insertDom = document.getElementById('netWorthDetails'); if (insertDom && !document.getElementById('assetDeltaContainer')) { window.kbd_calculateTotalNetworth?.( mwValues.equippedNetworth, mwValues.inventoryNetworth, mwValues.marketListingsNetworth, mwValues.totalHouseScore, mwValues.abilityScore, insertDom ); } }; window.addEventListener('load', () => { setTimeout(checkAssetsAndRun, 5000); }); if (document.readyState === 'complete') { setTimeout(checkAssetsAndRun, 5000); } setInterval(checkAssetsAndRun, 60000); })();