您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在商店计算自动计算最大时利润,在合同、交易所展示最大时利润
// ==UserScript== // @name 自动计算最大时利润 // @namespace https://github.com/gangbaRuby // @version 1.18.0 // @license AGPL-3.0 // @description 在商店计算自动计算最大时利润,在合同、交易所展示最大时利润 // @author Rabbit House // @match *://www.simcompanies.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=simcompanies.com // @grant GM_xmlhttpRequest // ==/UserScript== (function () { 'use strict'; // ====================== // 计算用到的函数 // ====================== let zn, lwe; //使用SimcompaniesConstantsData内数据 let size, acceleration, economyState, resource, salesModifierWithRecreationBonus, skillCMO, skillCOO, saturation, administrationOverhead, wages, buildingKind, forceQuality, cogs, quality, quantity const Ul = (overhead, skillCOO) => { const r = overhead || 1; return r - (r - 1) * skillCOO / 100; }; const wv = (e, t, r) => { return r === null ? lwe[e][t] : lwe[e][t].quality[r] } const Upt = (e, t, r, n) => t + (e + n) / r; const Hpt = (e, t, r, n, a) => { const o = (n + e) / ((t - a) * (t - a)); return e - (r - t) * (r - t) * o; }; const qpt = (e, t, r, n, a = 1) => (a * ((n - t) * 3600) - r) / (e + r); const Bpt = (e, t, r, n, a, o) => { const g = zn.RETAIL_ADJUSTMENT[e] ?? 1; const s = Math.min(Math.max(2 - n, 0), 2), l = Math.max(0.9, s / 2 + 0.5), c = r / 12; const d = zn.PROFIT_PER_BUILDING_LEVEL * (t.buildingLevelsNeededPerUnitPerHour * t.modeledUnitsSoldAnHour + 1) * g * (s / 2 * (1 + c * zn.RETAIL_MODELING_QUALITY_WEIGHT)) + (t.modeledStoreWages ?? 0); // console.log(`t.buildingLevelsNeededPerUnitPerHour:${t.buildingLevelsNeededPerUnitPerHour}, t.modeledUnitsSoldAnHour:${t.modeledUnitsSoldAnHour}, t.modeledStoreWages:${t.modeledStoreWages} , s:${s} , c:${c}, g:${g}`) const h = t.modeledUnitsSoldAnHour * l; const p = Upt(d, t.modeledProductionCostPerUnit, h, t.modeledStoreWages ?? 0); const m = Hpt(d, p, o, t.modeledStoreWages ?? 0, t.modeledProductionCostPerUnit); return qpt(m, t.modeledProductionCostPerUnit, t.modeledStoreWages ?? 0, o, a); }; const zL = (buildingKind, modeledData, quantity, salesModifier, price, qOverride, saturation, acc, size, weather) => { const u = Bpt(buildingKind, modeledData, qOverride, saturation, quantity, price); if (u <= 0) return NaN; const d = u / acc / size; let p = d - d * salesModifier / 100; return weather && (p /= weather.sellingSpeedMultiplier), p }; // 映射表 const resourceIdNameMap = { 1: "电力", 2: "水", 3: "苹果", 4: "橘子", 5: "葡萄", 6: "谷物", 7: "牛排", 8: "香肠", 9: "鸡蛋", 10: "原油", 11: "汽油", 12: "柴油", 13: "运输单位", 14: "矿物", 15: "铝土矿", 16: "硅材", 17: "化合物", 18: "铝材", 19: "塑料", 20: "处理器", 21: "电子元件", 22: "电池", 23: "显示屏", 24: "智能手机", 25: "平板电脑", 26: "笔记本电脑", 27: "显示器", 28: "电视机", 29: "作物研究", 30: "能源研究", 31: "采矿研究", 32: "电器研究", 33: "畜牧研究", 34: "化学研究", 35: "软件", 36: "undefined", 37: "undefined", 38: "undefined", 39: "undefined", 40: "棉花", 41: "棉布", 42: "铁矿石", 43: "钢材", 44: "沙子", 45: "玻璃", 46: "皮革", 47: "车载电脑", 48: "电动马达", 49: "豪华车内饰", 50: "基本内饰", 51: "车身", 52: "内燃机", 53: "经济电动车", 54: "豪华电动车", 55: "经济燃油车", 56: "豪华燃油车", 57: "卡车", 58: "汽车研究", 59: "时装研究", 60: "内衣", 61: "手套", 62: "裙子", 63: "高跟鞋", 64: "手袋", 65: "运动鞋", 66: "种子", 67: "圣诞爆竹", 68: "金矿石", 69: "金条", 70: "名牌手表", 71: "项链", 72: "甘蔗", 73: "乙醇", 74: "甲烷", 75: "碳纤维", 76: "碳纤复合材", 77: "机身", 78: "机翼", 79: "精密电子元件", 80: "飞行计算机", 81: "座舱", 82: "姿态控制器", 83: "火箭燃料", 84: "燃料储罐", 85: "固体燃料助推器", 86: "火箭发动机", 87: "隔热板", 88: "离子推进器", 89: "喷气发动机", 90: "亚轨道二级火箭", 91: "亚轨道火箭", 92: "轨道助推器", 93: "星际飞船", 94: "BFR", 95: "喷气客机", 96: "豪华飞机", 97: "单引擎飞机", 98: "无人机", 99: "人造卫星", 100: "航空航天研究", 101: "钢筋混凝土", 102: "砖块", 103: "水泥", 104: "黏土", 105: "石灰石", 106: "木材", 107: "钢筋", 108: "木板", 109: "窗户", 110: "工具", 111: "建筑预构件", 112: "推土机", 113: "材料研究", 114: "机器人", 115: "牛", 116: "猪", 117: "牛奶", 118: "咖啡豆", 119: "咖啡粉", 120: "蔬菜", 121: "面包", 122: "芝士", 123: "苹果派", 124: "橙汁", 125: "苹果汁", 126: "姜汁汽水", 127: "披萨", 128: "面条", 129: "汉堡包", 130: "千层面", 131: "肉丸", 132: "混合果汁", 133: "面粉", 134: "黄油", 135: "糖", 136: "可可", 137: "面团", 138: "酱汁", 139: "动物饲料", 140: "巧克力", 141: "植物油", 142: "沙拉", 143: "咖喱角", 144: "圣诞装饰品", 145: "食谱", 146: "南瓜", 147: "杰克灯笼", 148: "女巫服", 149: "南瓜汤", 150: "树", 151: "复活节兔兔", 152: "斋月糖果", 153: "巧克力冰淇淋", 154: "苹果冰淇淋" }; // ====================== // 模块1:网络请求模块 // ====================== const Network = (() => { // 通用请求方法 const makeRequest = (method, url, responseType, retryCount) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: method, url: url, headers: { 'Content-Type': 'application/json' }, onload: res => { try { resolve( responseType === 'json' ? JSON.parse(res.responseText) : res.responseText ); } catch (err) { if (retryCount > 0) { console.warn(`解析错误 ${url}, 重试中... (${retryCount})`); makeRequest(method, url, responseType, retryCount - 1) .then(resolve) .catch(reject); } else { reject(`最终解析失败: ${err}`); } } }, onerror: err => { if (retryCount > 0) { console.warn(`请求错误 ${url}, 重试中... (${retryCount})`); makeRequest(method, url, responseType, retryCount - 1) .then(resolve) .catch(reject); } else { reject(`最终请求失败: ${err}`); } } }); }); }; return { // 获取JSON数据(原有功能) requestJson: (url, retryCount = 3) => makeRequest('GET', url, 'json', retryCount), // 新增:获取原始文本(新功能) requestRaw: (url, retryCount = 3) => makeRequest('GET', url, 'text', retryCount) }; })(); // ====================== // 模块2:领域数据模块 // ====================== const RegionData = (() => { // 公司信息 const getAuthInfo = async () => { const data = await Network.requestJson('https://www.simcompanies.com/api/v3/companies/auth-data/'); return { realmId: data.authCompany?.realmId, companyId: data.authCompany?.companyId, company: data.authCompany?.company, salesModifier: data.authCompany?.salesModifier, economyState: data.temporals?.economyState, acceleration: data.levelInfo?.acceleration?.multiplier }; }; // 休闲加成 const getRecreationBonus = async (realmId, company) => { const formattedCompany = company.replace(/ /g, "-"); const data = await Network.requestJson( `https://www.simcompanies.com/api/v3/companies-by-company/${realmId}/${formattedCompany}/` ); return data.infrastructure?.recreationBonus; }; // 高管技能 const getExecutives = async () => { const response = await Network.requestJson('https://www.simcompanies.com/api/v3/companies/me/executives/'); const data = response.executives; const threeHoursAgo = Date.now() - 3 * 60 * 60 * 1000; // 定义职位代码映射 const targetPositions = ['o', 'f', 'm', 't', 'v', 'y']; return data.filter(exec => exec.currentWorkHistory && targetPositions.includes(exec.currentWorkHistory.position) && (!exec.strikeUntil || new Date(exec.strikeUntil) < new Date()) && new Date(exec.currentWorkHistory.start) < threeHoursAgo && !exec.currentTraining ); }; // 管理费 const getAdministrationCost = async () => { return Network.requestJson('https://www.simcompanies.com/api/v2/companies/me/administration-overhead/'); }; // 饱和度 const getResourcesRetailInfo = async (realmId) => { const data = await Network.requestJson( `https://www.simcompanies.com/api/v4/${realmId}/resources-retail-info/` ); const resourcesRetailInfo = []; // 遍历每个数据项并将对应的数据组合在一起 data.forEach(item => { resourcesRetailInfo.push({ quality: item.quality, dbLetter: item.dbLetter, averagePrice: item.averagePrice, saturation: item.saturation }); }); // console.log(resourcesRetailInfo); return resourcesRetailInfo; } // 天气 const getWeather = async (realmId) => { try { const data = await Network.requestJson(`https://www.simcompanies.com/api/v2/weather/${realmId}/`); return { Until: data.until, sellingSpeedMultiplier: data.sellingSpeedMultiplier }; } catch (e) { console.warn(`[Weather] Failed to fetch weather for realm ${realmId}:`, e); return { Until: null, sellingSpeedMultiplier: null }; } }; // 完整领域数据获取 const fetchFullRegionData = async () => { const auth = await getAuthInfo(); const [recreation, executives, administration, resourcesRetailInfo, sellingSpeedMultiplier, weatherUntil] = await Promise.all([ getRecreationBonus(auth.realmId, auth.company), getExecutives(), getAdministrationCost(), getResourcesRetailInfo(auth.realmId), getWeather(auth.realmId) ]); // 计算高管加成 const calculateExecutiveBonus = (executives) => { // 整理职位 → 技能表 const skills = executives.reduce((acc, exec) => { if (exec.currentWorkHistory) { acc[exec.currentWorkHistory.position] = exec.skills; } return acc; }, {}); // 安全读取技能值,没值就返回0 const safeSkill = (position, skillName) => skills[position]?.[skillName] || 0; let saleBonus = Math.floor(safeSkill('m', 'cmo') + safeSkill('y', 'cmo') / 2 + (safeSkill('o', 'cmo') + safeSkill('f', 'cmo') + safeSkill('t', 'cmo')) / 4); if (saleBonus > 80) { saleBonus = 80 + Math.floor((saleBonus - 80) / 2); } if (saleBonus > 60) { saleBonus = 60 + Math.floor((saleBonus - 60) / 2); } saleBonus = Math.floor(saleBonus / 3) let adminBonus = Math.floor(safeSkill('o', 'coo') + safeSkill('v', 'coo') / 2 + (safeSkill('f', 'coo') + safeSkill('m', 'coo') + safeSkill('t', 'coo')) / 4); if (adminBonus > 80) { adminBonus = 80 + Math.floor((adminBonus - 80) / 2); } if (adminBonus > 60) { adminBonus = 60 + Math.floor((adminBonus - 60) / 2); } return { saleBonus, adminBonus }; }; return { ...auth, recreationBonus: recreation, ...calculateExecutiveBonus(executives), administration, ResourcesRetailInfo: resourcesRetailInfo, sellingSpeedMultiplier, weatherUntil, timestamp: new Date().toISOString() }; }; return { fetchFullRegionData, getCurrentRealmId: async () => (await getAuthInfo()).realmId }; })(); // ====================== // 模块3:基本数据模块 // ====================== const constantsData = (() => { // 私有变量存储处理后的内容 let _processedData = null; // 获取并处理数据的逻辑 const init = async () => { try { const scriptTag = document.querySelector( 'script[type="module"][crossorigin][src^="https://www.simcompanies.com/static/bundle/assets/index-"][src$=".js"]' ); if (!scriptTag) throw new Error('未找到基本数据文件'); // 获取原始内容 const rawContent = await Network.requestRaw(scriptTag.src); // 空数据 const data = {}; // 需要提取core的数据键列表 const targetKeys = [ 'AVERAGE_SALARY', 'SALES', 'PROFIT_PER_BUILDING_LEVEL', 'RETAIL_MODELING_QUALITY_WEIGHT', 'RETAIL_ADJUSTMENT' ]; // 提取变量值(支持数字 / 布尔 / 对象) const extractValue = (variableName) => { const escapedVar = variableName.replace('$', '\\$'); const varRegex = new RegExp(`[,{\\s]${escapedVar}\\s*=\\s*([^,;\\n\\r]+)`); const match = rawContent.match(varRegex); if (!match) { console.warn(`变量未找到: ${variableName}`); return null; } try { const value = match[1].trim(); if (value.startsWith('{')) { const objectRegex = new RegExp(`[,{\\s]${escapedVar}\\s*=\\s*(\\{[^}]*\\})`); const matchAgain = rawContent.match(objectRegex); if (matchAgain) { return JSON.parse(matchAgain[1] .replace(/([{,]\s*|\{\s*)([^\s":,{}]+)(?=\s*:)/g, '$1"$2"') .replace(/:(\s*)\.(\d+)/g, ':$10.$2') ); } } return JSON.parse(value.replace(/^\.(\d+)/, '0.$1')); } catch { return match[1].trim(); } }; // 遍历 targetKeys,从 rawContent 中提取变量名并解析值 targetKeys.forEach(key => { const keyMatch = rawContent.match( new RegExp(`\\b${key}\\s*:\\s*([\\w$]+)`, 'm') ); if (keyMatch) { const varName = keyMatch[1]; data[key] = extractValue(varName); // 如果是 SALES,删掉 B 和 r 即删除大楼和餐馆此类非传统零售 if (key === 'SALES' && data[key]) { delete data[key]['B']; delete data[key]['r']; } } else { console.warn(`${key} 未找到`); } }); // 提取建筑工资系数 function extractSalaryModifiers(str) { const result = {}; // ✅ 处理第一种格式:多个变量赋值 const varAssignRegex = /(\w+)\s*=\s*{/g; let match; while ((match = varAssignRegex.exec(str)) !== null) { const startIndex = varAssignRegex.lastIndex - 1; let braceCount = 1; let currentIndex = startIndex + 1; while (braceCount > 0 && currentIndex < str.length) { if (str[currentIndex] === '{') braceCount++; else if (str[currentIndex] === '}') braceCount--; currentIndex++; } if (braceCount === 0) { const objText = str.slice(startIndex, currentIndex); const dbLetterMatch = objText.match(/dbLetter\s*:\s*"(\w+)"/); const salaryMatch = objText.match(/salaryModifier\s*:\s*([.\d]+)/); if (dbLetterMatch && salaryMatch) { const dbLetter = dbLetterMatch[1]; const salary = parseFloat(salaryMatch[1]); result[dbLetter] = salary; } } } // ✅ 处理第二种格式:对象字面量内部嵌套对象(带数字键) const objectEntryRegex = /\d+\s*:\s*{[\s\S]*?}/g; const entries = str.match(objectEntryRegex) || []; for (const entry of entries) { const dbLetterMatch = entry.match(/dbLetter\s*:\s*"(\w+)"/); const salaryMatch = entry.match(/salaryModifier\s*:\s*([.\d]+)/); if (dbLetterMatch && salaryMatch) { const dbLetter = dbLetterMatch[1]; const salary = parseFloat(salaryMatch[1]); result[dbLetter] = salary; } } return result; } const buildingsSalaryModifier = extractSalaryModifiers(rawContent); // 提取物品不同周期的基本参数 const extractJSONData = (str) => { // 匹配形如 "0: JSON.parse('...')" 或者 "0: JSON.parse(...)" 形式 const regex = /(\d+):\s*JSON\.parse\((['"])(.*?)\2\)/g; const retailInfo = {}; // 使用 matchAll 进行全局匹配 for (const match of str.matchAll(regex)) { const index = match[1]; // 捕获数字索引(0、1、2) const jsonData = match[3]; // 获取 JSON.parse() 中的内容 // console.log('周期:' + index + ', 内容:' + jsonData); try { // 直接解析 JSON 内容 const parsedData = JSON.parse(jsonData); // 将解析结果存入 retailInfo retailInfo[index] = parsedData; } catch (error) { console.error("JSON 解析错误:", error, "数据:", jsonData); } } return retailInfo; } const retailInfo = extractJSONData(rawContent); //提取物品基本数据 const extractMntFromRaw = (str) => { const assignPattern = /(\w+)\s*=\s*{/g; let match; while ((match = assignPattern.exec(rawContent)) !== null) { const startIndex = match.index + match[0].indexOf('{'); let braceCount = 1; let endIndex = startIndex + 1; while (braceCount > 0 && endIndex < rawContent.length) { const char = rawContent[endIndex]; if (char === '{') braceCount++; else if (char === '}') braceCount--; endIndex++; } if (braceCount === 0) { const objectString = rawContent.slice(startIndex, endIndex); try { const obj = eval('(' + objectString + ')'); if ( obj[1] && obj[1].dbLetter !== undefined && obj[150] && obj[150].producedFrom && obj[150].image?.includes("tree.png") ) { return obj; } } catch (e) { } } } return null; } const constantsResources = JSON.parse(JSON.stringify(extractMntFromRaw(rawContent))); return { data: data, buildingsSalaryModifier: buildingsSalaryModifier, retailInfo: retailInfo, constantsResources: constantsResources, timestamp: new Date().toISOString() }; } catch (error) { console.error('初始化失败:', error); throw error; } }; // 返回可访问处理结果的接口 return { initialize: init, getData: () => _processedData }; })(); // ====================== // 模块4:数据存储模块 // ====================== const Storage = (() => { const KEYS = { region: realmId => `SimcompaniesRetailCalculation_${realmId}`, constants: 'SimcompaniesConstantsData' }; const formatTime = (isoString) => { if (!isoString) return '无数据'; const d = new Date(isoString); return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')} ${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; }; return { save: (type, data) => { const key = type === 'region' ? KEYS.region(data.realmId) : KEYS.constants; localStorage.setItem(key, JSON.stringify(data)); }, getFormattedStatus: (type) => { try { let data; switch (type) { case 'r1': data = localStorage.getItem(KEYS.region(0)); break; case 'r2': data = localStorage.getItem(KEYS.region(1)); break; case 'constants': data = localStorage.getItem(KEYS.constants); break; } const parsedData = data ? JSON.parse(data) : null; return { text: parsedData ? formatTime(parsedData.timestamp) : '无数据', className: parsedData ? 'SimcompaniesRetailCalculation-has-data' : 'SimcompaniesRetailCalculation-no-data' }; } catch (error) { return { text: '数据损坏', className: 'SimcompaniesRetailCalculation-no-data' }; } } }; })(); // ====================== // 模块5:界面模块 // ====================== const PanelUI = (() => { let panelElement = null; const statusElements = {}; const typeDisplayNames = { r1: 'R1', r2: 'R2', constants: '基本' }; // 插入样式 const injectStyles = () => { const style = document.createElement('style'); style.textContent = ` .SimcompaniesRetailCalculation-mini-panel { position: fixed; left: 10px; bottom: 55px; z-index: 9999; font-family: Arial, sans-serif; } .SimcompaniesRetailCalculation-trigger-btn { width: 32px; height: 32px; background: #4CAF50; border-radius: 50%; border: none; cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: center; color: white; font-size: 18px; } .SimcompaniesRetailCalculation-panel-content { display: none; position: absolute; bottom: 40px; left: 0; background: rgba(40,40,40,0.95); border-radius: 4px; padding: 8px; min-width: 260px; box-shadow: 0 2px 8px rgba(0,0,0,0.3); } .SimcompaniesRetailCalculation-data-row { margin: 6px 0; font-size: 13px; display: flex; justify-content: space-between; align-items: center; } .SimcompaniesRetailCalculation-region-label { color: #BDBDBD; min-width: 70px; } .SimcompaniesRetailCalculation-region-status { font-family: monospace; margin-left: 10px; text-align: right; flex-grow: 1; } .SimcompaniesRetailCalculation-btn-group { margin-top: 8px; display: grid; gap: 6px; } .SimcompaniesRetailCalculation-action-btn { background: #2196F3; border: none; color: white; padding: 6px 10px; border-radius: 3px; cursor: pointer; font-size: 12px; white-space: nowrap; } .SimcompaniesRetailCalculation-action-btn:disabled { background: #607D8B; cursor: not-allowed; } .SimcompaniesRetailCalculation-no-data { color: #f44336; } .SimcompaniesRetailCalculation-has-data { color: #4CAF50; } `; document.head.appendChild(style); }; // 饱和度表格功能 let saturationTableElement = null; const showSaturationTable = () => { if (saturationTableElement) { saturationTableElement.remove(); saturationTableElement = null; return; } const realmId = getRealmIdFromLink(); if (realmId === null) { alert("未识别到 realmId!"); return; } const dataStr = localStorage.getItem(`SimcompaniesRetailCalculation_${realmId}`); if (!dataStr) { alert(`没有找到领域 ${realmId} 数据,请先更新!`); return; } const data = JSON.parse(dataStr); const list = data.ResourcesRetailInfo; const weatherSellingSpeedMultiplier = data.sellingSpeedMultiplier.sellingSpeedMultiplier // 表格 const table = document.createElement("table"); table.style.cssText = "border-collapse:collapse;margin:10px 0;background:#333;color:white;font-size:13px;"; const thead = document.createElement("thead"); const headerRow = document.createElement("tr"); ["物品", "质量", "饱和度"].forEach(text => { const th = document.createElement("th"); th.textContent = text; th.style.cssText = "border:1px solid #666;padding:4px 8px;"; headerRow.appendChild(th); }); thead.appendChild(headerRow); table.appendChild(thead); const tbody = document.createElement("tbody"); list.forEach(item => { const row = document.createElement("tr"); const name = resourceIdNameMap[item.dbLetter] || `未知(${item.dbLetter})`; [name, item.quality ?? "-", String(item.saturation)].forEach(text => { const td = document.createElement("td"); td.textContent = text; td.style.cssText = "border:1px solid #666;padding:4px 8px;text-align:center;"; row.appendChild(td); }); tbody.appendChild(row); }); table.appendChild(tbody); // 在表格上方插入 multiplier 显示 const multiplierRow = document.createElement("div"); multiplierRow.textContent = `天气销售速度倍率: ${weatherSellingSpeedMultiplier}`; multiplierRow.style.cssText = ` margin-bottom:6px; font-size:14px; font-weight:bold; color:#f1c40f; text-align:left; `; // 容器 saturationTableElement = document.createElement("div"); saturationTableElement.style.cssText = ` position:fixed; left:10px; top:50px; z-index:9998; background:#2c2c2c; color:#fff; padding:12px; border-radius:8px; max-height:400px; overflow:auto; box-shadow:0 4px 15px rgba(0,0,0,0.5); font-family:Arial, sans-serif; `; // 关闭按钮 const closeBtn = document.createElement("button"); closeBtn.textContent = "×"; closeBtn.style.cssText = ` position:absolute; top:6px; right:6px; background:#e74c3c; color:white; border:none; border-radius:50%; width:24px; height:24px; font-size:16px; cursor:pointer; line-height:24px; text-align:center; padding:0; transition: background 0.2s; `; closeBtn.onmouseover = () => closeBtn.style.background = "#ff6666"; closeBtn.onmouseout = () => closeBtn.style.background = "#e74c3c"; closeBtn.onclick = () => { saturationTableElement.remove(); saturationTableElement = null; }; saturationTableElement.appendChild(closeBtn); // 插入 multiplier 行 saturationTableElement.appendChild(multiplierRow); // 表格 table.style.background = "#333"; table.style.color = "#fff"; saturationTableElement.appendChild(table); document.body.appendChild(saturationTableElement); }; // 创建界面元素 const createPanel = () => { const panel = document.createElement('div'); panel.className = 'SimcompaniesRetailCalculation-mini-panel'; // 触发器按钮 const trigger = document.createElement('button'); trigger.className = 'SimcompaniesRetailCalculation-trigger-btn'; trigger.textContent = '≡'; trigger.addEventListener('click', togglePanel); // 内容面板 const content = document.createElement('div'); content.className = 'SimcompaniesRetailCalculation-panel-content'; // 状态显示行 const createStatusRow = (type) => { const row = document.createElement('div'); row.className = 'SimcompaniesRetailCalculation-data-row'; const label = document.createElement('span'); label.className = 'SimcompaniesRetailCalculation-region-label'; // 使用映射后的显示名称 label.textContent = `${typeDisplayNames[type]}数据:`; const status = document.createElement('span'); status.className = 'SimcompaniesRetailCalculation-region-status'; statusElements[type] = status; row.append(label, status); return row; }; // 操作按钮 const createActionButton = (text, type) => { const btn = document.createElement('button'); btn.className = 'SimcompaniesRetailCalculation-action-btn'; btn.textContent = text; btn.dataset.actionType = type; return btn; }; content.append( createStatusRow('r1'), createStatusRow('r2'), createStatusRow('constants') ); const btnGroup = document.createElement('div'); btnGroup.className = 'SimcompaniesRetailCalculation-btn-group'; btnGroup.append( createActionButton('更新领域数据', 'region'), createActionButton('更新基本数据', 'constants'), createActionButton('计算剩余量', 'calculateDecay'), (() => { const btn = document.createElement('button'); btn.className = 'SimcompaniesRetailCalculation-action-btn'; btn.textContent = '当前领域天气和饱和度表'; btn.onclick = showSaturationTable; return btn; })(), (() => { const btn = document.createElement('button'); btn.className = 'SimcompaniesRetailCalculation-action-btn'; btn.textContent = 'MP-?%'; btn.dataset.actionType = 'mpShow'; return btn; })() ); content.appendChild(btnGroup); // 插件信息区块 const info = document.createElement('div'); info.style.cssText = 'margin-top:10px;padding:8px;font-size:12px;line-height:1.5;color:#ccc;border-top:1px solid #555;'; const version = GM_info?.script?.version || '未知版本'; info.innerHTML = ` 作者:<a href="https://www.simcompanies.com/zh-cn/company/0/Rabbit-House/" target="_blank" style="color:#6cf;">Rabbit House</a> 反馈请说明问题<br> 源码:<a href="https://github.com/gangbaRuby/SimCompanies-Scripts" target="_blank" style="color:#6cf;">GitHub</a> ⭐🙇<br> 版本:${version} `; content.appendChild(info); panel.append(trigger, content); return panel; }; // 切换面板可见性 const togglePanel = (e) => { e.stopPropagation(); const content = panelElement.querySelector('.SimcompaniesRetailCalculation-panel-content'); content.style.display = content.style.display === 'block' ? 'none' : 'block'; refreshStatus(); }; // 刷新状态显示 const refreshStatus = () => { ['r1', 'r2', 'constants'].forEach(type => { const { text, className } = Storage.getFormattedStatus(type); statusElements[type].textContent = text; statusElements[type].className = `SimcompaniesRetailCalculation-region-status ${className}`; }); }; const MpPanel = (() => { let inputPercent = (() => { const val = localStorage.getItem('mp_inputPercent'); return val === null ? 2.5 : parseFloat(val); })(); // 监听url变化,自动更新面板内容和标题 function addUrlChangeListener(callback) { let lastUrl = location.href; new MutationObserver(() => { const url = location.href; if (url !== lastUrl) { lastUrl = url; callback(url); } }).observe(document, { subtree: true, childList: true }); } // 获取当前资源ID(路径中提取) function getCurrentResourceId() { const url = location.pathname; const match = url.match(/\/market\/resource\/(\d+)(\/|$)/); return match ? match[1] : null; } // 监听调用 addUrlChangeListener(() => { updateContent('请点击计算'); const titleEl = document.querySelector('#mp-floating-box div:first-child div'); if (titleEl) { titleEl.textContent = `MP-?% - 点合同时利润降序,点公司跳转私信`; } }); function renderResultTable(results) { if (!Array.isArray(results) || results.length === 0) { return '<p>无数据</p>'; } const headers = ['卖家', '市场价', '品质', '数量', '合同价', '合同时利润']; let html = '<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse; width: 100%;">'; // 普通表头,不带sticky样式 html += '<thead><tr>' + headers.map((h, i) => `<th class="th-${i}">${h}</th>`).join('') + '</tr></thead>'; html += '<tbody>'; for (const row of results) { html += '<tr>' + `<td style="max-width:120px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"> <a href="https://www.simcompanies.com/zh-cn/messages/${encodeURIComponent(row.seller)}" target="_blank" style="color: inherit; text-decoration: none; display: inline-block; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"> ${row.seller} </a> </td>` + `<td>${row.marketPrice}</td>` + `<td>${row.quality}</td>` + `<td>${row.saleAmout}</td>` + `<td>${row.contractPrice.toFixed(2)}</td>` + `<td>${row.contractMaxProfit}</td>` + '</tr>'; } html += '</tbody></table>'; return html; } // 插入表格后调用此函数绑定样式和排序事件 function enableTableFeatures() { const table = document.querySelector('#mp-table-container table'); if (!table) return; const profitTh = table.querySelector('thead th.th-5'); if (!profitTh) return; let ascending = false; // 默认降序 profitTh.style.cursor = 'pointer'; profitTh.onclick = () => { const tbody = table.querySelector('tbody'); const rows = Array.from(tbody.querySelectorAll('tr')); rows.sort((a, b) => { const aVal = parseFloat(a.cells[5].textContent) || 0; const bVal = parseFloat(b.cells[5].textContent) || 0; return ascending ? aVal - bVal : bVal - aVal; }); rows.forEach(row => tbody.appendChild(row)); ascending = !ascending; }; } // 面板显示和初始化 function showPanel() { let box = document.getElementById('mp-floating-box'); if (box) { box.style.display = box.style.display === 'none' ? 'block' : 'none'; updateContent('点击“计算”开始计算'); return; } box = document.createElement('div'); box.id = 'mp-floating-box'; box.style.cssText = ` position: fixed; left: 25px; top: 50px; width: min(450px, 90vw); max-height: 70vh; background: #222; color: #eee; padding: 12px; border-radius: 6px; box-shadow: 0 0 15px rgba(0,0,0,0.7); z-index: 9998; overflow: hidden; font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; font-size: 14px; white-space: normal; word-break: break-word; user-select: none; display: flex; flex-direction: column; `; // header const header = document.createElement('div'); header.style.cssText = ` cursor: move; padding: 6px 10px; background: #111; border-radius: 6px 6px 0 0; font-weight: bold; user-select: none; display: flex; align-items: center; justify-content: space-between; `; const title = document.createElement('div'); title.textContent = `MP-?% - 点合同时利润降序,点公司跳转私信`; header.appendChild(title); const closeBtn = document.createElement('span'); closeBtn.textContent = '✖'; closeBtn.title = '关闭'; closeBtn.style.cssText = ` cursor: pointer; font-weight: bold; color: #aaa; user-select: none; margin-left: 10px; `; closeBtn.onmouseenter = () => (closeBtn.style.color = '#fff'); closeBtn.onmouseleave = () => (closeBtn.style.color = '#aaa'); closeBtn.onclick = () => (box.style.display = 'none'); header.appendChild(closeBtn); box.appendChild(header); // 输入区 const inputWrapper = document.createElement('div'); inputWrapper.style.cssText = 'display: flex; align-items: center; gap: 8px; margin: 10px 0; color: #eee; font-weight: bold;'; inputWrapper.innerHTML = ` <span style="flex: 0 0 auto;">MP-</span> <input id="mp-percent-input" type="number" min="0" step="0.1" value="${inputPercent}" style="background: #2c3e50; color: #fff; width: 40px;"> <span style="flex: 0 0 auto;">% 输入负数为直接减去</span> <button id="mp-calc-btn" style="background: #2196F3; color: white; flex: 0 0 auto; margin-left: 12px; cursor: pointer;">计算</button> `; box.appendChild(inputWrapper); // 提示区 const content = document.createElement('div'); content.id = 'mp-floating-content'; content.style.cssText = ` flex-shrink: 0; height: 28px; line-height: 28px; overflow: hidden; margin-top: 8px; color: #eee; white-space: nowrap; text-overflow: ellipsis; `; box.appendChild(content); // 表格容器 const tableContainer = document.createElement('div'); tableContainer.id = 'mp-table-container'; tableContainer.style.cssText = ` flex-grow: 1; margin-top: 8px; max-height: 320px; /* 你可以调节这个高度 */ overflow-y: auto; `; box.appendChild(tableContainer); document.body.appendChild(box); // 表格样式:固定第一列,其他列自适应 const style = document.createElement('style'); style.textContent = ` #mp-table-container table { width: 100%; table-layout: fixed; word-break: break-word; } #mp-table-container table th:first-child, #mp-table-container table td:first-child { width: 50px; text-align: center; } #mp-floating-box div { flex-wrap: wrap; /* 小屏幕自动换行 */ } #mp-floating-box input, #mp-floating-box button, #mp-floating-box span { flex-shrink: 1; /* 缩小避免撑出 */ } `; document.head.appendChild(style); // 计算按钮事件 const calcBtn = document.getElementById('mp-calc-btn'); const percentInput = document.getElementById('mp-percent-input'); calcBtn.addEventListener('click', async () => { calcBtn.disabled = true; inputPercent = parseFloat(percentInput.value) || 0; localStorage.setItem('mp_inputPercent', inputPercent); const realm = getRealmIdFromLink(); const resourceId = getCurrentResourceId(); const name = resourceIdNameMap[resourceId] || `未知(${resourceId})`; if (realm === null || resourceId === null) { updateContent('无法确定 realmId 或 resourceId'); calcBtn.disabled = false; return; } const raw = localStorage.getItem(`market_${realm}_${resourceId}`); if (!raw) { updateContent('无市场数据,无法计算'); calcBtn.disabled = false; return; } let data; try { data = JSON.parse(raw); } catch { updateContent('市场数据解析错误'); calcBtn.disabled = false; return; } updateContent('计算中,请稍候...'); document.getElementById('mp-table-container').innerHTML = ''; // 清空表格区域 try { if (!window.MarketInterceptor || !window.MarketInterceptor.calculateProfit) { updateContent('计算服务未准备好'); calcBtn.disabled = false; return; } const result = await window.MarketInterceptor.calculateProfit(inputPercent, data, getRealmIdFromLink()); updateContent(`计算完成,当前产品为:${name}`); document.getElementById('mp-table-container').innerHTML = renderResultTable(result); enableTableFeatures(); } catch (e) { updateContent('计算发生错误'); console.error(e); } finally { calcBtn.disabled = false; } }); updateContent('请输入参数,点击计算'); dragElement(box, header); } function updateContent(text) { const content = document.getElementById('mp-floating-content'); if (!content) return; content.textContent = text; } // 外部调用入口 return { showPanel }; })(); // 拖拽函数,复制自已有代码 const dragElement = (elmnt, dragHandle) => { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; dragHandle.onmousedown = dragMouseDown; function dragMouseDown(e) { e.preventDefault(); pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } function elementDrag(e) { e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; let newTop = elmnt.offsetTop - pos2; let newLeft = elmnt.offsetLeft - pos1; newTop = Math.max(0, Math.min(window.innerHeight - elmnt.offsetHeight, newTop)); newLeft = Math.max(0, Math.min(window.innerWidth - elmnt.offsetWidth, newLeft)); elmnt.style.top = newTop + 'px'; elmnt.style.left = newLeft + 'px'; } function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } }; // 处理数据更新 const handleUpdate = async (type) => { const button = panelElement.querySelector(`[data-action-type="${type}"]`); if (type === 'mpShow') { MpPanel.showPanel(); return; } if (type === 'calculateDecay') { button.disabled = true; button.textContent = '计算中...'; const wasOpen = document.getElementById('decayDataPanel')?.style.display !== 'none'; try { await window.calculateAll(); // 先执行计算 } catch (e) { console.error('计算失败', e); } finally { if (wasOpen) { DecayResultViewer.show(); // 如果原本是打开的,就刷新 } else { DecayResultViewer.toggle(); // 原本关闭,执行 toggle 打开 } button.disabled = false; button.textContent = '计算剩余量'; } return; } try { button.disabled = true; button.textContent = '更新中...'; let data; if (type === 'region') { const realmId = await RegionData.getCurrentRealmId(); data = await RegionData.fetchFullRegionData(); Storage.save('region', data); } else { data = await constantsData.initialize(); Storage.save('constants', data); } refreshStatus(); } catch (error) { console.error(`${type}更新失败:`, error); statusElements[type === 'region' ? 'r1' : 'constants'].textContent = '更新失败'; statusElements[type === 'region' ? 'r1' : 'constants'].className = 'SimcompaniesRetailCalculation-region-status SimcompaniesRetailCalculation-no-data'; } finally { button.disabled = false; button.textContent = type === 'region' ? '更新领域数据' : '更新基本数据'; } }; return { init() { injectStyles(); panelElement = createPanel(); document.body.appendChild(panelElement); // 事件委托处理按钮点击 panelElement.addEventListener('click', (e) => { if (e.target.closest('[data-action-type]')) { const type = e.target.dataset.actionType; handleUpdate(type); } }); // 点击外部关闭面板 document.addEventListener('click', (e) => { if (!panelElement.contains(e.target)) { panelElement.querySelector('.SimcompaniesRetailCalculation-panel-content').style.display = 'none'; } }); // 初始状态刷新 refreshStatus(); } }; })(); // 初始化界面 PanelUI.init(); // ====================== // 模块6:商店内的最大时利润 本模块只使用了SimcompaniesConstantsData // ====================== (function () { // setInput: 输入并触发 input 事件 function setInput(inputNode, value, count = 3) { let lastValue = inputNode.value; inputNode.value = value; let event = new Event("input", { bubbles: true }); event.simulated = true; if (inputNode._valueTracker) inputNode._valueTracker.setValue(lastValue); inputNode.dispatchEvent(event); if (count >= 0) return setInput(inputNode, value, --count); } // 获取 React 组件 function findReactComponent(element) { // 动态匹配所有可能的 React 内部属性 const reactKeys = Object.keys(element).filter(key => key.startsWith('__reactInternalInstance') || key.startsWith('__reactFiber') ); for (const key of reactKeys) { let fiberNode = element[key]; while (fiberNode) { if (fiberNode.stateNode?.updateProfitPerUnit) { return fiberNode.stateNode; } fiberNode = fiberNode.return; } } return null; } // 主功能 function initAutoPricing() { try { const input = document.querySelector('input[name="price"]'); if (!input) { // console.warn("[AutoPricing] Price input not found!"); return; } const reactInstance = findReactComponent(input); if (!reactInstance) { console.warn("[AutoPricing] React component not found!", Object.keys(input)); return; } const cards = document.querySelectorAll('div[style="overflow: visible;"]'); cards.forEach(card => { if (card.dataset.autoPricingAdded) return; const priceInput = card.querySelector('input[name="price"]'); if (!priceInput) return; const comp = findReactComponent(priceInput); if (!comp) return; const btn = document.createElement('button'); btn.textContent = '最大时利润'; btn.type = 'button'; btn.style = ` margin-top: 5px; background: #2196F3; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; width: 100%; `; btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); if (localStorage.getItem('SimcompaniesConstantsData') == null) { alert("请尝试更新基本数据(左下角按钮)"); return; } lwe = JSON.parse(localStorage.getItem("SimcompaniesConstantsData")).retailInfo; zn = JSON.parse(localStorage.getItem("SimcompaniesConstantsData")).data; // 直接从comp.props赋值 size = comp.props.size; acceleration = comp.props.acceleration; economyState = comp.props.economyState; resource = comp.props.resource; salesModifierWithRecreationBonus = comp.props.salesModifierWithRecreationBonus; skillCMO = comp.props.skillCMO; skillCOO = comp.props.skillCOO; saturation = comp.props.saturation; administrationOverhead = comp.props.administrationOverhead; wages = comp.props.wages; buildingKind = comp.props.buildingKind; forceQuality = comp.props.forceQuality; // 直接从comp.state赋值 cogs = comp.state.cogs; quality = comp.state.quality; quantity = comp.state.quantity; // console.log(`size:${size}, acceleration:${acceleration}, economyState:${economyState}, // resource:${resource},salesModifierWithRecreationBonus:${salesModifierWithRecreationBonus}, // skillCMO:${skillCMO}, skillCOO:${skillCOO}, // saturation:${saturation}, administrationOverhead:${administrationOverhead}, wages:${wages}, // buildingKind:${buildingKind}, forceQuality:${forceQuality},cogs:${cogs}, quality:${quality}, quantity:${quantity}`) // console.log(`zn.PROFIT_PER_BUILDING_LEVEL: ${zn.PROFIT_PER_BUILDING_LEVEL}`) let currentPrice = Math.floor(cogs / quantity) || 1; let bestPrice = currentPrice; let maxProfit = -Infinity; let _, v, b, w, revenue, wagesTotal, secondsToFinish, currentWagesTotal = 0; // console.log(`currentPrice:${currentPrice}, bestPrice:${bestPrice}, maxProfit:${maxProfit}`) // setInput(input, currentPrice.toFixed(2)); // 以下两个不受currentPrice影响 可不参与循环 v = salesModifierWithRecreationBonus + Math.floor(skillCMO / 3); b = Ul(administrationOverhead, skillCOO); while (currentPrice > 0) { w = zL(buildingKind, wv(economyState, resource.dbLetter, (_ = forceQuality) != null ? _ : null), parseFloat(quantity), v, currentPrice, forceQuality === void 0 ? quality : 0, saturation, acceleration, size, resource.retailSeason === "Summer" ? comp.props.weather : void 0); // console.log(`v:${v}, b:${b}, w:${w}`) revenue = currentPrice * quantity; wagesTotal = Math.ceil(w * wages * acceleration * b / 60 / 60); secondsToFinish = w; // console.log(`revenue:${revenue}, wagesTotal:${wagesTotal}, secondsToFinish:${secondsToFinish}`) if (!secondsToFinish || secondsToFinish <= 0) break; let profit = (revenue - cogs - wagesTotal) / secondsToFinish; if (profit > maxProfit) { maxProfit = profit; bestPrice = currentPrice; } else if (maxProfit > 0 && profit < 0) { //有正利润后出现负利润提前终端循环 break; } // console.log(`当前定价:${bestPrice}, 当前最大秒利润:${maxProfit}`) if (currentPrice < 8) { currentPrice = Math.round((currentPrice + 0.01) * 100) / 100; } else if (currentPrice < 2001) { currentPrice = Math.round((currentPrice + 0.1) * 10) / 10; } else { currentPrice = Math.round(currentPrice + 1); } } setInput(priceInput, bestPrice.toFixed(2)); // 先移除旧的 maxProfit 显示(避免重复) const oldProfit = card.querySelector('.auto-profit-display'); if (oldProfit) oldProfit.remove(); // 创建新的 maxProfit 显示元素 const profitDisplay = document.createElement('div'); profitDisplay.className = 'auto-profit-display'; profitDisplay.textContent = `每级时利润: ${((maxProfit / size) * 3600).toFixed(2)}`; profitDisplay.style = ` margin-top: 5px; font-size: 14px; color: white; background: gray; padding: 4px 8px; text-align: center; `; // 插入按钮下方 btn.parentNode.insertBefore(profitDisplay, btn.nextSibling); // 校验用 如果误差大则提示用户尝试更新数据 currentWagesTotal = Math.ceil(zL(buildingKind, wv(economyState, resource.dbLetter, (_ = forceQuality) != null ? _ : null), parseFloat(quantity), v, bestPrice, forceQuality === void 0 ? quality : 0, saturation, acceleration, size, resource.retailSeason === "Summer" ? comp.props.weather : void 0) * wages * acceleration * b / 60 / 60); // console.log(`currentWagesTotal:${currentWagesTotal}, comp.state.wagesTotal: ${comp.state.wagesTotal}`) if (currentWagesTotal !== comp.state.wagesTotal) { alert("计算利润与显示利润不相符,请先输入数量或请尝试更新基本数据(左下角按钮)"); } }; priceInput.parentNode.insertBefore(btn, priceInput.nextSibling); card.dataset.autoPricingAdded = 'true'; }); } catch (err) { // console.error("[AutoPricing] Critical error:", err); } } // 启动观察器,只在商品卡片变化时运行自动定价逻辑 function observeCardsForAutoPricing() { // 防抖计时器 let debounceTimer; // 目标容器 - 改为更具体的容器选择器(如果能确定的话) const targetNode = document.body; // 或者更具体的容器如 '#shop-container' // 优化后的观察器配置 const observer = new MutationObserver((mutationsList) => { // 使用防抖避免频繁触发 clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { // 检查是否有新增的卡片节点 const hasNewCards = mutationsList.some(mutation => { return mutation.type === 'childList' && mutation.addedNodes.length > 0 && Array.from(mutation.addedNodes).some(node => { return node.nodeType === 1 && // 元素节点 (node.matches('div[style="overflow: visible;"]') || node.querySelector('div[style="overflow: visible;"]')); }); }); if (hasNewCards) { initAutoPricing(); } }, 100); // 100ms防抖延迟 }); // 优化观察配置 observer.observe(targetNode, { childList: true, // 观察直接子节点的添加/删除 subtree: true, // 观察所有后代节点 attributes: false, // 不需要观察属性变化 characterData: false // 不需要观察文本变化 }); // 初始执行(使用requestAnimationFrame确保DOM已加载) requestAnimationFrame(() => { initAutoPricing(); }); } observeCardsForAutoPricing(); })(); // ====================== // 模块7:交易所计算时利润 使用SimcompaniesRetailCalculation_{realmId} SimcompaniesConstantsData // ====================== const ResourceMarketHandler = (function () { let currentResourceId = null; let currentRealmId = null; let rowIdCounter = 0; const pendingRows = new Map(); // rowId -> <tr> element // Create worker blob: calculations move into worker's onmessage const workerCode = ` self.onmessage = function(e) { const { rowId, order, SCD, SRC } = e.data; const { price, quantity, quality, resourceId: resource } = order; // bring constants into worker scope const lwe = SCD.retailInfo; const zn = SCD.data; // Utility functions defined inside to use local lwe and zn const Ul = (overhead, skillCOO) => { const r = overhead || 1; return r - (r - 1) * skillCOO / 100; }; const wv = (e, t, r) => { return r === null ? lwe[e][t] : lwe[e][t].quality[r]; }; const Upt = (e, t, r, n) => t + (e + n) / r; const Hpt = (e, t, r, n, a) => { const o = (n + e) / ((t - a) * (t - a)); return e - (r - t) * (r - t) * o; }; const qpt = (e, t, r, n, a = 1) => (a * ((n - t) * 3600) - r) / (e + r); const Bpt = (e, t, r, n, a, o) => { const g = zn.RETAIL_ADJUSTMENT[e] ?? 1; const s = Math.min(Math.max(2 - n, 0), 2), l = Math.max(0.9, s / 2 + 0.5), c = r / 12; const d = zn.PROFIT_PER_BUILDING_LEVEL * (t.buildingLevelsNeededPerUnitPerHour * t.modeledUnitsSoldAnHour + 1) * g * (s / 2 * (1 + c * zn.RETAIL_MODELING_QUALITY_WEIGHT)) + (t.modeledStoreWages ?? 0); const h = t.modeledUnitsSoldAnHour * l; const p = Upt(d, t.modeledProductionCostPerUnit, h, t.modeledStoreWages ?? 0); const m = Hpt(d, p, o, t.modeledStoreWages ?? 0, t.modeledProductionCostPerUnit); return qpt(m, t.modeledProductionCostPerUnit, t.modeledStoreWages ?? 0, o, a); }; const zL = (buildingKind, modeledData, quantity, salesModifier, price, qOverride, saturation, acc, size, weather) => { const u = Bpt(buildingKind, modeledData, qOverride, saturation, quantity, price); if (u <= 0) return NaN; const d = u / acc / size; let p = d - d * salesModifier / 100; return weather && (p /= weather.sellingSpeedMultiplier), p }; // Initial debug log // profit calculation loop let currentPrice = price, maxProfit = -Infinity, size = 1, acceleration = SRC.acceleration, economyState = SRC.economyState, salesModifierWithRecreationBonus = SRC.salesModifier + SRC.recreationBonus, skillCMO = SRC.saleBonus, skillCOO = SRC.adminBonus; // compute saturation locally const saturation = (() => { const list = SRC.ResourcesRetailInfo; const m = list.find(item => item.dbLetter === parseInt(resource) && (parseInt(resource) !== 150 || item.quality === quality) ); return m?.saturation; })(); const administrationOverhead = SRC.administration; const buildingKind = Object.entries(zn.SALES).find(([k, ids]) => ids.includes(parseInt(resource)) )?.[0]; const salaryModifier = SCD.buildingsSalaryModifier?.[buildingKind]; const averageSalary = zn.AVERAGE_SALARY; const wages = averageSalary * salaryModifier; const forceQuality = (parseInt(resource) === 150) ? quality : undefined; const resourceDetail = SCD.constantsResources[parseInt(resource)] const v = salesModifierWithRecreationBonus + skillCMO; const b = Ul(administrationOverhead, skillCOO); let selltime; while (currentPrice > 0) { const modeledData = wv(economyState, resource, forceQuality ?? null); const w = zL( buildingKind, modeledData, quantity, v, currentPrice, forceQuality === void 0 ? quality : 0, saturation, acceleration, size, resourceDetail.retailSeason === "Summer" ? SRC.sellingSpeedMultiplier : void 0 ); const revenue = currentPrice * quantity; const wagesTotal = Math.ceil(w * wages * acceleration * b / 3600); const secondsToFinish = w; const profit = (!secondsToFinish || secondsToFinish <= 0) ? NaN : (revenue - price * quantity - wagesTotal) / secondsToFinish; if (!secondsToFinish || secondsToFinish <= 0) break; if (profit > maxProfit) { maxProfit = profit; selltime = secondsToFinish; } else if (maxProfit > 0 && profit < 0) { break; } // price increment if (currentPrice < 8) { currentPrice = Math.round((currentPrice + 0.01) * 100) / 100; } else if (currentPrice < 2001) { currentPrice = Math.round((currentPrice + 0.1) * 10) / 10; } else { currentPrice = Math.round(currentPrice + 1); } } self.postMessage({ rowId, maxProfit, selltime}); }; `; const profitWorker = new Worker(URL.createObjectURL(new Blob([workerCode], { type: 'application/javascript' }))); // 全局状态与注册器(放最上面,只运行一次) const allProfitSpans = new Set(); let isShowingProfit = true; setInterval(() => { isShowingProfit = !isShowingProfit; for (const span of allProfitSpans) { const { profitText, timeText } = span.dataset; span.textContent = isShowingProfit ? profitText : timeText; } }, 3000); // 主回调处理 profitWorker.onmessage = function (e) { const { rowId, maxProfit, selltime } = e.data; const hours = Math.floor(selltime / 3600); const minutes = Math.ceil((selltime % 3600) / 60); const timeStr = `${hours > 0 ? `${hours}h ` : ''}${minutes}m`; const profit = (maxProfit * 3600).toFixed(2); const row = pendingRows.get(rowId); if (!row) return; pendingRows.delete(rowId); if (!row.querySelector('td.auto-profit-info')) { const td = document.createElement('td'); td.classList.add('auto-profit-info'); const span = document.createElement('span'); const isMobile = window.innerWidth <= 600; const profitText = `时利润:${Math.round(profit)}`; const timeText = `用时:${timeStr}`; const fullText = `时利润:${profit} 用时:${timeStr}`; span.textContent = isMobile ? (isShowingProfit ? profitText : timeText) : fullText; span.style.cssText = ` display: inline-block; min-width: 60px; font-size: 16px; color: white; background: gray; padding: 4px 8px; line-height: 1.2; box-sizing: border-box; `.trim(); td.appendChild(span); row.appendChild(td); if (isMobile) { span.dataset.profitText = profitText; span.dataset.timeText = timeText; allProfitSpans.add(span); } } }; function findValidTbody() { return [...document.querySelectorAll('tbody')].find(tbody => { const firstRow = tbody.querySelector('tr'); return firstRow && firstRow.children.length >= 4 && firstRow.querySelector('td > div > div > a[href*="/company/"]'); }); } function extractNumbersFromAriaLabel(label) { if (!label || typeof label !== 'string') return null; let nums = []; let lastThree = []; // 中文直接用原逻辑 if (/由.*公司提供/.test(label)) { const cleanedLabel = label.replace(/,/g, ''); nums = cleanedLabel.match(/[\d.]+/g); if (!nums || nums.length < 3) return null; lastThree = nums.slice(-3).map(x => Number(x)); } // 英文处理 else if (/market order/i.test(label)) { // 提取公司名位置 const companyMatch = label.match(/offered by company\s+([^\.,,]*)/i); const companyStart = companyMatch ? companyMatch.index : label.length; // 只取公司名前的文本进行数字匹配,避免公司名里的点或数字干扰 const textToParse = label.slice(0, companyStart).replace(/,/g, ''); nums = textToParse.match(/[\d.]+/g); if (!nums || nums.length < 3) return null; lastThree = nums.slice(-3).map(x => Number(x)); } const [price, quantity, quality] = lastThree; if ([price, quantity, quality].some(n => isNaN(n))) return null; return { price, quantity, quality }; } function extractRealmIdOnce(tbody) { if (currentRealmId) return; const row = tbody.querySelector('tr'); const link = row?.querySelector('a[href*="/company/"]'); const match = link?.getAttribute('href')?.match(/\/company\/(\d+)\//); if (match) { currentRealmId = match[1]; // console.log('领域ID:', currentRealmId); } } function formatSeconds(seconds) { const h = Math.floor(seconds / 3600).toString().padStart(2, '0'); const m = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0'); const s = Math.floor(seconds % 60).toString().padStart(2, '0'); return `${h}:${m}:${s}`; } async function processNewRows(tbody) { const salesMap = JSON.parse(localStorage.getItem("SimcompaniesConstantsData")).data.SALES; const rows = Array.from(tbody.querySelectorAll('tr')) .filter(r => !r.querySelector('td.auto-profit-info') && !r.hasAttribute('data-profit-calculated')); rows.forEach(row => { const ariaData = extractNumbersFromAriaLabel(row.getAttribute('aria-label') || ''); if (!ariaData) return; // 过滤非零售商品 const isRetail = Object.values(salesMap).some(list => list.includes(parseInt(currentResourceId))); if (!isRetail) return; const order = { resourceId: currentResourceId, realmId: currentRealmId, ...ariaData }; const SCD = JSON.parse(localStorage.getItem("SimcompaniesConstantsData")); const SRC = JSON.parse(localStorage.getItem(`SimcompaniesRetailCalculation_${order.realmId}`)); if (!SCD || !SRC) return; if (rowIdCounter > 99999) rowIdCounter = 0; const rowId = rowIdCounter++; pendingRows.set(rowId, row); row.setAttribute('data-profit-calculated', '1'); // 防止重复处理 profitWorker.postMessage({ rowId, order, SCD, SRC }); }); } return { init(resourceId) { currentResourceId = resourceId; currentRealmId = null; let observer; function tryInit() { const tbody = findValidTbody(); if (!tbody) return; if (observer) observer.disconnect(); // 👉 插入到form中 const form = document.querySelector('form'); if (form) { const parentDiv = form.parentElement; // form 的直接父级 <div> const container = parentDiv?.parentElement?.parentElement; // css-rnlot4 的容器 if (container && !container.querySelector('[data-custom-notice]')) { const infoText = document.createElement('div'); infoText.textContent = '高管、周期变动,会影响计算,记得更新,所有展示内容均为1级建筑。'; infoText.dataset.customNotice = 'true'; // 避免重复添加 container.appendChild(infoText); // 插入在 form 所在 div 的后面 } } const initPromise = (() => { extractRealmIdOnce(tbody); const salesMap = JSON.parse(localStorage.getItem("SimcompaniesConstantsData")).data.SALES; const isRetail = Object.values(salesMap).some(list => list.includes(parseInt(currentResourceId))); if (!isRetail) return Promise.resolve(); // 如果不是零售商品,跳过处理 return processNewRows(tbody); // 是零售商品就处理新行 })(); initPromise .then(() => { // 不需要重复调用 extract 和 process,如果上面处理过了 }) .catch(console.error); const rowObserver = new MutationObserver(() => processNewRows(tbody)); rowObserver.observe(tbody, { childList: true, subtree: true }); } tryInit(); observer = new MutationObserver(tryInit); observer.observe(document, { childList: true, subtree: true }); } }; })(); // ====================== // 模块8:合同计算时利润 使用SimcompaniesRetailCalculation_{realmId} SimcompaniesConstantsData // ====================== const incomingContractsHandler = (function () { let cardIdCounter = 0; const pendingCards = new Map(); // cardId -> DOM element // Worker 代码 const workerCode = ` self.onmessage = function(e) { const { cardId, order, SCD, SRC } = e.data; const { price, quantity, quality, resourceId: resource } = order; const lwe = SCD.retailInfo; const zn = SCD.data; const Ul = (overhead, skillCOO) => overhead - (overhead - 1) * skillCOO / 100; const wv = (e, t, r) => r === null ? lwe[e][t] : lwe[e][t].quality[r]; const Upt = (e, t, r, n) => t + (e + n) / r; const Hpt = (e, t, r, n, a) => { const o = (n + e) / ((t - a) * (t - a)); return e - (r - t) * (r - t) * o; }; const qpt = (e, t, r, n, a = 1) => (a * ((n - t) * 3600) - r) / (e + r); const Bpt = (e, t, r, n, a, o) => { const g = zn.RETAIL_ADJUSTMENT[e] ?? 1; const s = Math.min(Math.max(2 - n, 0), 2), l = Math.max(0.9, s / 2 + 0.5), c = r / 12; const d = zn.PROFIT_PER_BUILDING_LEVEL * (t.buildingLevelsNeededPerUnitPerHour * t.modeledUnitsSoldAnHour + 1) * g * (s / 2 * (1 + c * zn.RETAIL_MODELING_QUALITY_WEIGHT)) + (t.modeledStoreWages ?? 0); const h = t.modeledUnitsSoldAnHour * l; const p = Upt(d, t.modeledProductionCostPerUnit, h, t.modeledStoreWages ?? 0); const m = Hpt(d, p, o, t.modeledStoreWages ?? 0, t.modeledProductionCostPerUnit); return qpt(m, t.modeledProductionCostPerUnit, t.modeledStoreWages ?? 0, o, a); }; const zL = (buildingKind, modeledData, quantity, salesModifier, price, qOverride, saturation, acc, size, weather) => { const u = Bpt(buildingKind, modeledData, qOverride, saturation, quantity, price); if (u <= 0) return NaN; const d = u / acc / size; let p = d - d * salesModifier / 100; return weather && (p /= weather.sellingSpeedMultiplier), p }; let currentPrice = price, maxProfit = -Infinity, size = 1, acceleration = SRC.acceleration, economyState = SRC.economyState, salesModifierWithRecreationBonus = SRC.salesModifier + SRC.recreationBonus, skillCMO = SRC.saleBonus, skillCOO = SRC.adminBonus; const saturation = (() => { const list = SRC.ResourcesRetailInfo; const m = list.find(item => item.dbLetter === parseInt(resource) && (parseInt(resource) !== 150 || item.quality === quality) ); return m?.saturation; })(); const administrationOverhead = SRC.administration; const buildingKind = Object.entries(zn.SALES).find(([k, ids]) => ids.includes(parseInt(resource)) )?.[0]; const salaryModifier = SCD.buildingsSalaryModifier?.[buildingKind]; const averageSalary = zn.AVERAGE_SALARY; const wages = averageSalary * salaryModifier; const forceQuality = (parseInt(resource) === 150) ? quality : undefined; const resourceDetail = SCD.constantsResources[parseInt(resource)] const v = salesModifierWithRecreationBonus + skillCMO; const b = Ul(administrationOverhead, skillCOO); while (currentPrice > 0) { const modeledData = wv(economyState, resource, forceQuality ?? null); const w = zL( buildingKind, modeledData, quantity, v, currentPrice, forceQuality === void 0 ? quality : 0, saturation, acceleration, size, resourceDetail.retailSeason === "Summer" ? SRC.sellingSpeedMultiplier : void 0 ); const revenue = currentPrice * quantity; const wagesTotal = Math.ceil(w * wages * acceleration * b / 3600); const secondsToFinish = w; const profit = (!secondsToFinish || secondsToFinish <= 0) ? NaN : (revenue - price * quantity - wagesTotal) / secondsToFinish; if (!secondsToFinish || secondsToFinish <= 0) break; if (profit > maxProfit) { maxProfit = profit; } else if (maxProfit > 0 && profit < 0) { break; } if (currentPrice < 8) { currentPrice = Math.round((currentPrice + 0.01) * 100) / 100; } else if (currentPrice < 2001) { currentPrice = Math.round((currentPrice + 0.1) * 10) / 10; } else { currentPrice = Math.round(currentPrice + 1); } } self.postMessage({ cardId, maxProfit }); }; `; const profitWorker = new Worker(URL.createObjectURL(new Blob([workerCode], { type: 'application/javascript' }))); profitWorker.onmessage = function (e) { const { cardId, maxProfit } = e.data; const card = pendingCards.get(cardId); if (!card) return; pendingCards.delete(cardId); injectHourlyProfit(card, maxProfit * 3600); }; function init() { // console.log('[合同页面处理] 初始化合同页面处理逻辑'); const checkPageLoaded = setInterval(() => { const isOnTargetPage = /^https:\/\/www\.simcompanies\.com(\/[a-z-]+)?\/headquarters\/warehouse\/incoming-contracts\/?$/.test(location.href); if (!isOnTargetPage) { // console.log('[合同页面处理] 用户已离开页面,停止轮询'); clearInterval(checkPageLoaded); removeWarningNotice(); // 🔄 页面离开时清理提示 return; } const contractCards = document.querySelectorAll('div[tabindex="0"]'); if (contractCards.length > 0) { // console.log('[合同页面处理] 合同卡片已加载'); clearInterval(checkPageLoaded); insertWarningNotice(); // ✅ 卡片加载后插入提示 contractCards.forEach(handleCard); startMutationObserver(); } else { // console.log('[合同页面处理] 等待合同卡片加载...'); } }, 500); } function startMutationObserver() { const targetNode = document.querySelectorAll('.row')[1]; if (!targetNode) { console.error('[合同页面处理] 未找到目标容器'); return; } const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { const contractCards = document.querySelectorAll('div[tabindex="0"]'); contractCards.forEach(handleCard); } } }); observer.observe(targetNode, { childList: true, subtree: true }); } function getRealmIdFromLink() { const link = document.querySelector('a[href*="/company/"]'); // 选择第一个符合条件的 <a> 标签 if (link) { const match = link.href.match(/\/company\/(\d+)\//); // 提取 href 中的 realmId return match ? parseInt(match[1], 10) : null; // 如果匹配到 realmId,返回 } return null; // 如果没有找到符合条件的链接,返回 null } function handleCard(card) { // ✅ 提前返回条件改成: if (card.hasAttribute('data-found') && !card.hasAttribute('data-retry')) return; const data = parseContractCard(card); if (!data || !data.dbLetter) return; const realmId = getRealmIdFromLink(); const constantsKey = 'SimcompaniesConstantsData'; const regionKey = `SimcompaniesRetailCalculation_${realmId}`; if (!localStorage.getItem(constantsKey) || !localStorage.getItem(regionKey)) { console.log('[合同卡片] 缺少数据,尝试初始化...'); card.setAttribute('data-retry', 'true'); // 👈 表明后续还要再处理 constantsData.initialize() .then(data => { Storage.save('constants', data); return RegionData.fetchFullRegionData(); }) .then(regionData => { Storage.save('region', regionData); console.log('[合同卡片] 数据初始化完成,重新处理卡片'); handleCard(card); // ✅ 数据准备好再重试 }) .catch(err => { console.error('[合同卡片] 数据初始化失败:', err); }); return; } card.setAttribute('data-found', 'true'); // ✅ 仅在数据准备好后设置 card.removeAttribute('data-retry'); const SCD = JSON.parse(localStorage.getItem(constantsKey)); const SRC = JSON.parse(localStorage.getItem(regionKey)); const isRetail = Object.values(SCD.data.SALES).some(arr => arr.includes(parseInt(data.dbLetter)) ); if (!isRetail) { console.log(`[合同卡片] 非零售商品,跳过处理: dbLetter=${data.dbLetter}`); return; } const cardId = cardIdCounter++; pendingCards.set(cardId, card); profitWorker.postMessage({ cardId, order: { resourceId: data.dbLetter, price: data.unitPrice, quantity: data.quantity, quality: data.quality }, SCD, SRC }); } function parseContractCard(card) { console.log(card) const result = { quantity: null, quality: null, unitPrice: null, totalPrice: null, imageSrc: null, resourcePath: null, dbLetter: null, }; const label = card.getAttribute('aria-label') || ''; if (/quality/i.test(label)) { // 英文处理 const quantityMatch = label.match(/(\d+)\s+[A-Za-z ]+quality/i); const qualityMatch = label.match(/quality\s*(\d+)/i); const unitPriceMatch = label.match(/at\s+\$([\d,.]+)\s+per unit/i); const totalPriceMatch = label.match(/total price\s+\$([\d,.]+)/i); if (quantityMatch) result.quantity = parseInt(quantityMatch[1].replace(/,/g, '')); if (qualityMatch) result.quality = parseInt(qualityMatch[1]); if (unitPriceMatch) result.unitPrice = parseFloat(unitPriceMatch[1].replace(/,/g, '')); if (totalPriceMatch) result.totalPrice = parseFloat(totalPriceMatch[1].replace(/,/g, '')); } else { // 中文原逻辑保留 const numberMatches = [...label.matchAll(/[\d,]+(?:\.\d+)?/g)]; const qMatch = label.match(/Q(\d+)/); if (numberMatches.length >= 3 && qMatch) { result.totalPrice = parseFloat(numberMatches[numberMatches.length - 1][0].replace(/,/g, '')); result.unitPrice = parseFloat(numberMatches[numberMatches.length - 2][0].replace(/,/g, '')); result.quantity = parseInt(numberMatches[numberMatches.length - 4][0].replace(/,/g, '')); result.quality = parseInt(qMatch[1]); } else { console.warn('[合同卡片] aria-label 数字匹配失败:', label); } } const img = card.querySelector('img[src^="/static/images/resources/"]'); if (img) { result.imageSrc = img.getAttribute('src'); result.resourcePath = result.imageSrc.replace(/^\/static\//, ''); const constants = JSON.parse(localStorage.getItem('SimcompaniesConstantsData') || '{}'); const resources = Object.values(constants?.constantsResources || {}); const matched = resources.find(r => r.image === result.resourcePath); if (matched) result.dbLetter = matched.dbLetter; } return result; } function injectHourlyProfit(card, profitValue) { const infoDiv = Array.from(card.querySelectorAll('div')) .find(div => div.textContent?.includes('@') && div.querySelector('b')); const priceBox = infoDiv?.querySelector('b'); if (!priceBox) return; if (priceBox.nextSibling?.nodeType === Node.ELEMENT_NODE && priceBox.nextSibling.textContent?.includes('时利润')) return; const profitDisplay = document.createElement('b'); profitDisplay.textContent = ` 时利润:${profitValue.toFixed(2)}`; profitDisplay.style.marginLeft = '8px'; priceBox.parentNode.insertBefore(profitDisplay, priceBox.nextSibling); } function insertWarningNotice() { if (document.querySelector('[data-warning-text]')) return; const cards = document.querySelectorAll('div[tabindex="0"]'); cards.forEach(card => { let parent = card.parentElement; if (!parent) return; let grandParent = parent.parentElement; if (!grandParent || grandParent.querySelector('[data-warning-text]')) return; const insertTarget = grandParent.firstElementChild; if (!insertTarget || insertTarget === parent) return; const tip = document.createElement('div'); tip.textContent = '高管若变动,时利润会有误差,点左下更新。'; tip.dataset.warningText = 'true'; insertTarget.appendChild(tip); }); } function removeWarningNotice() { const oldNotice = document.querySelector('[data-warning-text]'); if (oldNotice) oldNotice.remove(); } return { init }; })(); // ====================== // 模块9:判断当前页面 // ====================== (function () { const PAGE_ACTIONS = { marketPage: { pattern: /^https:\/\/www\.simcompanies\.com(?:\/[^\/]+)?\/market\/resource\/(\d+)\/?$/, action: (url) => { const match = url.match(/\/resource\/(\d+)\/?/); const resourceId = match ? match[1] : null; if (resourceId) { console.log('进入 market 页面,资源ID:', resourceId); ResourceMarketHandler.init(resourceId); } } }, contractPage: { pattern: /^https:\/\/www\.simcompanies\.com(?:\/[a-z-]+)?\/headquarters\/warehouse\/incoming-contracts\/?$/, action: (url) => { console.log('[合同页面识别] 已进入合同页面'); incomingContractsHandler.init(); } } }; function handlePage() { const url = location.href; for (const { pattern, action } of Object.values(PAGE_ACTIONS)) { if (pattern.test(url)) { action(url); return; } } } let lastUrl = ''; const observer = new MutationObserver(() => { if (lastUrl !== location.href) { lastUrl = location.href; handlePage(); } }); observer.observe(document, { subtree: true, childList: true }); handlePage(); })(); // ====================== // 模块10:自动或定时更新数据 SimcompaniesConstantsData SimcompaniesRetailCalculation超过一小时就更新 // 只在打开新标签页和切换领域是才会判断时间更新 更新数据无锁 // ====================== // 使用 MutationObserver 监听 DOM 变化并提取 realmId // 提取 realmId 的函数 function getRealmIdFromLink() { const link = document.querySelector('a[href*="/company/"]'); // 选择第一个符合条件的 <a> 标签 if (link) { const match = link.href.match(/\/company\/(\d+)\//); // 提取 href 中的 realmId return match ? parseInt(match[1], 10) : null; // 如果匹配到 realmId,返回 } return null; // 如果没有找到符合条件的链接,返回 null } // ConstantsAutoUpdater 用于更新常量数据 const ConstantsAutoUpdater = (() => { const STORAGE_KEY = 'SimcompaniesConstantsData'; const ONE_HOUR = 60 * 60 * 1000; const needsUpdate = () => { const dataStr = localStorage.getItem(STORAGE_KEY); if (!dataStr) return true; try { const data = JSON.parse(dataStr); const lastTime = new Date(data.timestamp).getTime(); const now = Date.now(); return now - lastTime > ONE_HOUR; } catch (e) { return true; } }; const update = async () => { try { const data = await constantsData.initialize(); Storage.save('constants', data); console.log('[ConstantsAutoUpdater] 基本数据已更新'); } catch (err) { console.error('[ConstantsAutoUpdater] 基本数据更新失败', err); } }; const checkAndUpdate = () => { if (needsUpdate()) { console.log('[ConstantsAutoUpdater] 开始更新基本数据...'); update(); } else { console.log('[ConstantsAutoUpdater] 基本数据是最新的'); } }; return { checkAndUpdate }; })(); // RegionAutoUpdater 用于更新领域数据 const RegionAutoUpdater = (() => { const ONE_HOUR = 60 * 60 * 1000; const needsUpdate = (realmId) => { const key = `SimcompaniesRetailCalculation_${realmId}`; const dataStr = localStorage.getItem(key); if (!dataStr) return true; try { const data = JSON.parse(dataStr); const lastTime = new Date(data.timestamp).getTime(); const weatherUntil = new Date(data.sellingSpeedMultiplier.weatherUntil).getTime(); const now = Date.now(); const ONE_HOUR = 60 * 60 * 1000; if (now - lastTime > ONE_HOUR) return true; //大于1小时 if (now > weatherUntil) return true; //天气过期 // 当前北京时间 const nowInBeijing = new Date(now + 8 * 60 * 60 * 1000); // 早上 7:45 的北京时间戳 7:30开始更新饱和度 保险起见7:45更新 也是保证新的一天的第一次更新 const todayBeijing = new Date(nowInBeijing.toISOString().slice(0, 10)); // 北京当天 0点 const morning745 = new Date(todayBeijing.getTime() + 7 * 60 * 60 * 1000 + 45 * 60 * 1000).getTime(); // 早上 22:01 的北京时间戳 高管获得经验的更新 const todayBeijing1 = new Date(nowInBeijing.toISOString().slice(0, 10)); // 北京当天 0点 const executives2201 = new Date(todayBeijing1.getTime() + 22 * 60 * 60 * 1000 + 1 * 60 * 1000).getTime(); // 本周五 23:01 的北京时间戳 const currentWeekday = nowInBeijing.getUTCDay(); // 周日是 0 const daysUntilFriday = (5 - currentWeekday + 7) % 7; const fridayDate = new Date(todayBeijing.getTime() + daysUntilFriday * 24 * 60 * 60 * 1000); const friday2301 = new Date(fridayDate.getTime() + 23 * 60 * 60 * 1000 + 1 * 60 * 1000).getTime(); const lastTimeInBeijing = lastTime + 8 * 60 * 60 * 1000; // 触发早上 7:45 的更新 if (now >= morning745 && lastTimeInBeijing < morning745) { return true; } // 触发晚上 22:01 的更新 if (now >= executives2201 && lastTimeInBeijing < executives2201) { return true; } // 触发周五 23:01 的更新 if (now >= friday2301 && lastTimeInBeijing < friday2301) { return true; } return false; } catch (e) { return true; } }; const update = async (realmId) => { try { let data; data = await RegionData.fetchFullRegionData(); Storage.save('region', data); console.log(`[RegionAutoUpdater] 领域数据(${realmId})已更新`); } catch (err) { console.error(`[RegionAutoUpdater] 领域数据(${realmId})更新失败`, err); } }; const checkAndUpdate = (realmId) => { if (realmId === null) { console.warn('[RegionAutoUpdater] 页面上无法识别 realmId'); return; } if (needsUpdate(realmId)) { console.log(`[RegionAutoUpdater] 开始更新领域数据(${realmId})...`); update(realmId); } else { console.log(`[RegionAutoUpdater] 领域数据(${realmId})是最新的`); } }; return { checkAndUpdate }; })(); // 首先执行 ConstantsAutoUpdater 的检查和更新 ConstantsAutoUpdater.checkAndUpdate(); // 然后执行 RegionAutoUpdater 的检查和更新 RegionAutoUpdater.checkAndUpdate(0); RegionAutoUpdater.checkAndUpdate(1); // ====================== // 模块11:计算预测剩余量 // ====================== (function () { // 计算入口函数(可被按钮触发调用) async function calculateAllDecayResources() { try { const realmId = getRealmIdFromLink(); const regionKey = `SimcompaniesRetailCalculation_${realmId}`; const SRC = JSON.parse(localStorage.getItem(regionKey)); if (!SRC || !SRC.companyId) { console.warn("[库存模块] 未找到 companyId,无法发起请求"); return; } const url = `https://www.simcompanies.com/api/v3/resources/${SRC.companyId}/`; const response = await fetch(url); const data = await response.json(); const now = Date.now(); const workerCode = ` self.onmessage = function(e) { const { data, now, companyId } = e.data; function fo(entry, t) { const n = Date.parse(entry.datetime); const a = Math.abs(t - n); const o = Math.round(a / (1e3 * 60) / 4) * 4 / 60; return Math.floor(entry.amount * Math.pow(1 - 0.05, o)); } function alignTimeToOriginalSeconds(originalTimeStr, nowTimestamp) { const originalDate = new Date(originalTimeStr); const nowDate = new Date(nowTimestamp); const originalSeconds = originalDate.getSeconds(); const originalMilliseconds = originalDate.getMilliseconds(); const alignedDate = new Date(nowDate); alignedDate.setSeconds(originalSeconds, originalMilliseconds); if (alignedDate.getTime() > nowTimestamp) { alignedDate.setMinutes(alignedDate.getMinutes() - 1); } return alignedDate.getTime(); } function formatLocalDateSimple(date) { const pad = (n) => String(n).padStart(2, '0'); return \`\${pad(date.getMonth() + 1)}-\${pad(date.getDate())} \${pad(date.getHours())}:\${pad(date.getMinutes())}:\${pad(Math.floor(date.getSeconds()))}\`; } function calculate(entry) { const decayTime = Date.parse(entry.datetime); const quantity = entry.amount; const totalCost = Object.values(entry.cost || {}).reduce((sum, v) => sum + (typeof v === 'number' ? v : 0), 0); let lastAmount = fo(entry, now); const results = []; let currentTime = alignTimeToOriginalSeconds(entry.datetime, now); for (; currentTime < decayTime + 8760 * 60 * 60 * 1000; currentTime += 1000) { const diff = Math.abs(currentTime - decayTime); const cycleCount = Math.round(diff / (1000 * 60) / 4) * 4 / 60; const amount = Math.floor(quantity * Math.pow(1 - 0.05, cycleCount)); if (amount !== lastAmount) { const dateStr = formatLocalDateSimple(new Date(currentTime)); const unitCost = amount === 0 ? Infinity : Number((totalCost / amount).toFixed(3)); results.push({ time: dateStr, amount, unitCost }); lastAmount = amount; if (amount === 0) break; } } return { kind: entry.kind, quality: entry.quality, result: results }; } const output = {}; for (const entry of data) { if ([153, 154].includes(entry.kind)) { if (!output[entry.kind]) output[entry.kind] = {}; if (!output[entry.kind][entry.quality]) { output[entry.kind][entry.quality] = calculate(entry); } } } self.postMessage({ companyId, output }); }; `; const blob = new Blob([workerCode], { type: 'application/javascript' }); const worker = new Worker(URL.createObjectURL(blob)); worker.onmessage = function (e) { const { companyId, output } = e.data; const key = `wareHouse-${companyId}`; localStorage.setItem(key, JSON.stringify(output)); window.dispatchEvent(new Event('warehouse-updated')); //console.log(`[📦资源剩余量已计算] ${key}`, output); }; worker.postMessage({ data, now, companyId: SRC.companyId }); } catch (e) { console.error("[库存模块] 处理失败:", e); } } async function calculateContractsOutgoing() { try { const realmId = getRealmIdFromLink(); const regionKey = `SimcompaniesRetailCalculation_${realmId}`; const SRC = JSON.parse(localStorage.getItem(regionKey)); if (!SRC || !SRC.companyId) { console.warn("[合同模块] 未找到 companyId,无法发起请求"); return; } const url = `https://www.simcompanies.com/api/v2/contracts-outgoing/`; const response = await fetch(url); const data = await response.json(); const now = Date.now(); const workerCode = ` self.onmessage = function(e) { const { data, now, companyId } = e.data; function fo(entry, t) { const n = Date.parse(entry.datetime); const a = Math.abs(t - n); const o = Math.round(a / (1e3 * 60) / 4) * 4 / 60; return Math.floor(entry.quantity * Math.pow(1 - 0.05, o)); } function alignTimeToOriginalSeconds(originalTimeStr, nowTimestamp) { const originalDate = new Date(originalTimeStr); const nowDate = new Date(nowTimestamp); const originalSeconds = originalDate.getSeconds(); const originalMilliseconds = originalDate.getMilliseconds(); const alignedDate = new Date(nowDate); alignedDate.setSeconds(originalSeconds, originalMilliseconds); if (alignedDate.getTime() > nowTimestamp) { alignedDate.setMinutes(alignedDate.getMinutes() - 1); } return alignedDate.getTime(); } function formatLocalDateSimple(date) { const pad = (n) => String(n).padStart(2, '0'); return \`\${pad(date.getMonth() + 1)}-\${pad(date.getDate())} \${pad(date.getHours())}:\${pad(date.getMinutes())}:\${pad(Math.floor(date.getSeconds()))}\`; } function calculate(entry) { const decayTime = Date.parse(entry.datetime); const quantity = entry.quantity; let lastAmount = fo(entry, now); const results = []; let currentTime = alignTimeToOriginalSeconds(entry.datetime, now); for (; currentTime < decayTime + 8760 * 60 * 60 * 1000; currentTime += 1000) { const diff = Math.abs(currentTime - decayTime); const cycleCount = Math.round(diff / (1000 * 60) / 4) * 4 / 60; const amount = Math.floor(quantity * Math.pow(1 - 0.05, cycleCount)); if (amount !== lastAmount) { const dateStr = formatLocalDateSimple(new Date(currentTime)); results.push({ time: dateStr, amount, }); lastAmount = amount; if (amount === 0) break; } } return { kind: entry.kind, buyer: entry.buyer.company, quality: entry.quality, quantity: entry.quantity, price: entry.price, datetime: entry.datetime, rawTime: decayTime, result: results }; } const output = {}; for (const entry of data) { if ([153, 154].includes(entry.kind) && entry.datetime) { if (!output[entry.kind]) output[entry.kind] = {}; if (!output[entry.kind][entry.buyer.company]) output[entry.kind][entry.buyer.company] = []; output[entry.kind][entry.buyer.company].push(calculate(entry)); } } self.postMessage({ companyId, output }); }; `; const blob = new Blob([workerCode], { type: 'application/javascript' }); const worker = new Worker(URL.createObjectURL(blob)); worker.onmessage = function (e) { const { companyId, output } = e.data; const key = `contractsOutgoing-${companyId}`; localStorage.setItem(key, JSON.stringify(output)); window.dispatchEvent(new Event('contractsOutgoing-updated')); //console.log(`[📦合同剩余量已计算] ${key}`, output); }; worker.postMessage({ data, now, companyId: SRC.companyId }); } catch (e) { console.error("[合同模块] 处理失败:", e); } } async function calculateContractsIncoming() { try { const realmId = getRealmIdFromLink(); const regionKey = `SimcompaniesRetailCalculation_${realmId}`; const SRC = JSON.parse(localStorage.getItem(regionKey)); if (!SRC || !SRC.companyId) { console.warn("[合同模块] 未找到 companyId,无法发起请求"); return; } const url = `https://www.simcompanies.com/api/v2/contracts-incoming/`; const response = await fetch(url); const json = await response.json(); const data = json.incomingContracts; const now = Date.now(); const workerCode = ` self.onmessage = function(e) { const { data, now, companyId } = e.data; function fo(entry, t) { const n = Date.parse(entry.datetime); const a = Math.abs(t - n); const o = Math.round(a / (1e3 * 60) / 4) * 4 / 60; return Math.floor(entry.quantity * Math.pow(1 - 0.05, o)); } function alignTimeToOriginalSeconds(originalTimeStr, nowTimestamp) { const originalDate = new Date(originalTimeStr); const nowDate = new Date(nowTimestamp); const originalSeconds = originalDate.getSeconds(); const originalMilliseconds = originalDate.getMilliseconds(); const alignedDate = new Date(nowDate); alignedDate.setSeconds(originalSeconds, originalMilliseconds); if (alignedDate.getTime() > nowTimestamp) { alignedDate.setMinutes(alignedDate.getMinutes() - 1); } return alignedDate.getTime(); } function formatLocalDateSimple(date) { const pad = (n) => String(n).padStart(2, '0'); return \`\${pad(date.getMonth() + 1)}-\${pad(date.getDate())} \${pad(date.getHours())}:\${pad(date.getMinutes())}:\${pad(Math.floor(date.getSeconds()))}\`; } function calculate(entry) { const decayTime = Date.parse(entry.datetime); const quantity = entry.quantity; let lastAmount = fo(entry, now); const results = []; let currentTime = alignTimeToOriginalSeconds(entry.datetime, now); for (; currentTime < decayTime + 8760 * 60 * 60 * 1000; currentTime += 1000) { const diff = Math.abs(currentTime - decayTime); const cycleCount = Math.round(diff / (1000 * 60) / 4) * 4 / 60; const amount = Math.floor(quantity * Math.pow(1 - 0.05, cycleCount)); if (amount !== lastAmount) { const dateStr = formatLocalDateSimple(new Date(currentTime)); results.push({ time: dateStr, amount, }); lastAmount = amount; if (amount === 0) break; } } return { kind: entry.kind, seller: entry.seller.company, quality: entry.quality, quantity: entry.quantity, price: entry.price, datetime: entry.datetime, rawTime: decayTime, result: results }; } const output = {}; for (const entry of data) { if ([153, 154].includes(entry.kind) && entry.datetime) { if (!output[entry.kind]) output[entry.kind] = {}; if (!output[entry.kind][entry.buyer.company]) output[entry.kind][entry.buyer.company] = []; output[entry.kind][entry.buyer.company].push(calculate(entry)); } } self.postMessage({ companyId, output }); }; `; const blob = new Blob([workerCode], { type: 'application/javascript' }); const worker = new Worker(URL.createObjectURL(blob)); worker.onmessage = function (e) { const { companyId, output } = e.data; const key = `contractsIncoming-${companyId}`; localStorage.setItem(key, JSON.stringify(output)); window.dispatchEvent(new Event('contractsIncoming-updated')); }; worker.postMessage({ data, now, companyId: SRC.companyId }); } catch (e) { console.error("[合同模块] 处理失败:", e); } } async function calculateMarket() { try { const realmId = getRealmIdFromLink(); const regionKey = `SimcompaniesRetailCalculation_${realmId}`; const SRC = JSON.parse(localStorage.getItem(regionKey)); if (!SRC || !SRC.companyId) { console.warn("[市场模块] 未找到 companyId,无法发起请求"); return; } const url = `https://www.simcompanies.com/api/v2/companies/${SRC.companyId}/market-orders/`; const response = await fetch(url); const data = await response.json(); const now = Date.now(); const workerCode = ` self.onmessage = function(e) { const { data, now, companyId } = e.data; function fo(entry, t) { const n = Date.parse(entry.datetimeDecayUpdated); const a = Math.abs(t - n); const o = Math.round(a / (1e3 * 60) / 4) * 4 / 60; return Math.floor(entry.quantity * Math.pow(1 - 0.05, o)); } function alignTimeToOriginalSeconds(originalTimeStr, nowTimestamp) { const originalDate = new Date(originalTimeStr); const nowDate = new Date(nowTimestamp); const originalSeconds = originalDate.getSeconds(); const originalMilliseconds = originalDate.getMilliseconds(); const alignedDate = new Date(nowDate); alignedDate.setSeconds(originalSeconds, originalMilliseconds); if (alignedDate.getTime() > nowTimestamp) { alignedDate.setMinutes(alignedDate.getMinutes() - 1); } return alignedDate.getTime(); } function formatLocalDateSimple(date) { const pad = (n) => String(n).padStart(2, '0'); return \`\${pad(date.getMonth() + 1)}-\${pad(date.getDate())} \${pad(date.getHours())}:\${pad(date.getMinutes())}:\${pad(Math.floor(date.getSeconds()))}\`; } function calculate(entry) { const decayTime = Date.parse(entry.datetimeDecayUpdated); const quantity = entry.quantity; let lastAmount = fo(entry, now); const results = []; let currentTime = alignTimeToOriginalSeconds(entry.datetimeDecayUpdated, now); for (; currentTime < decayTime + 8760 * 60 * 60 * 1000; currentTime += 1000) { const diff = Math.abs(currentTime - decayTime); const cycleCount = Math.round(diff / (1000 * 60) / 4) * 4 / 60; const amount = Math.floor(quantity * Math.pow(1 - 0.05, cycleCount)); if (amount !== lastAmount) { const dateStr = formatLocalDateSimple(new Date(currentTime)); results.push({ time: dateStr, amount, }); lastAmount = amount; if (amount === 0) break; } } return { kind: entry.kind, quality: entry.quality, price: entry.price, result: results }; } const output = {}; for (const entry of data) { if ([153, 154].includes(entry.kind) && entry.datetimeDecayUpdated) { if (!output[entry.kind]) output[entry.kind] = {}; if (!output[entry.kind][entry.quality]) output[entry.kind][entry.quality] = {}; if (!output[entry.kind][entry.quality][entry.price]) { output[entry.kind][entry.quality][entry.price] = calculate(entry); } } } self.postMessage({ companyId, output }); }; `; const blob = new Blob([workerCode], { type: 'application/javascript' }); const worker = new Worker(URL.createObjectURL(blob)); worker.onmessage = function (e) { const { companyId, output } = e.data; const key = `marketOrders-${companyId}`; localStorage.setItem(key, JSON.stringify(output)); window.dispatchEvent(new Event('marketOrders-updated')); //console.log(`[📦市场剩余量已计算] ${key}`, output); }; worker.postMessage({ data, now, companyId: SRC.companyId }); } catch (e) { console.error("[市场模块] 处理失败:", e); } } async function calculateAll() { await calculateAllDecayResources(); await calculateContractsOutgoing(); await calculateContractsIncoming(); await calculateMarket(); } // 暴露到 window 供外部按钮调用 window.calculateAll = calculateAll; })(); // ====================== // 模块12:展示预测剩余量 // ====================== const DecayResultViewer = (() => { let container, header, content; const KIND_NAMES = { 153: '巧克力冰淇凌', 154: '苹果冰淇凌', }; const getCurrentCompanyData = () => { const realmId = getRealmIdFromLink(); const regionKey = `SimcompaniesRetailCalculation_${realmId}`; const SRC = JSON.parse(localStorage.getItem(regionKey)); if (!SRC || !SRC.companyId) { console.warn("[资源模块] 未找到 companyId,无法展示资源面板"); return { inventory: [], market: [], contract: [] }; } const inventoryKey = `wareHouse-${SRC.companyId}`; const marketKey = `marketOrders-${SRC.companyId}`; const contractsOutgoingKey = `contractsOutgoing-${SRC.companyId}`; const contractsIncomingKey = `contractsIncoming-${SRC.companyId}`; const inventory = []; const market = []; let contractsOutgoing = {}; let contractsIncoming = {}; const rawInventory = localStorage.getItem(inventoryKey); if (rawInventory) { try { const obj = JSON.parse(rawInventory); for (const kind in obj) { for (const quality in obj[kind]) { inventory.push(obj[kind][quality]); } } } catch (e) { console.warn('解析库存数据失败', e); } } const rawMarket = localStorage.getItem(marketKey); if (rawMarket) { try { const obj = JSON.parse(rawMarket); for (const kind in obj) { for (const quality in obj[kind]) { for (const price in obj[kind][quality]) { market.push(obj[kind][quality][price]); } } } } catch (e) { console.warn('解析市场数据失败', e); } } const rawContractsOutgoing = localStorage.getItem(contractsOutgoingKey); if (rawContractsOutgoing) { try { contractsOutgoing = JSON.parse(rawContractsOutgoing); } catch (e) { console.warn('解析出库合同数据失败', e); } } const rawContractsIncoming = localStorage.getItem(contractsIncomingKey); if (rawContractsIncoming) { try { contractsIncoming = JSON.parse(rawContractsIncoming); } catch (e) { console.warn('解析入库合同数据失败', e); } } return { inventory, market, contractsOutgoing, contractsIncoming }; }; const getDataFromStorage = () => { const data = getCurrentCompanyData(); return data; }; const formatSimpleDate = (dateStr) => { const d = new Date(dateStr); const pad = (n) => String(n).padStart(2, '0'); return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; }; const createToggleSection = (title, contentElement, isOpen = true) => { const section = document.createElement("div"); section.style.marginBottom = '8px'; const header = document.createElement("div"); header.textContent = (isOpen ? '▼ ' : '▶ ') + title; header.style.cssText = "cursor:pointer;font-weight:bold;padding:6px;background:#444;border-radius:4px;user-select:none;"; header.addEventListener("click", () => { const isHidden = contentElement.style.display === "none"; contentElement.style.display = isHidden ? "block" : "none"; header.textContent = (isHidden ? '▼ ' : '▶ ') + title; }); section.appendChild(header); section.appendChild(contentElement); contentElement.style.display = isOpen ? "block" : "none"; return section; }; const renderResult = () => { const data = getDataFromStorage(); content.innerHTML = ''; // 清空内容 content.appendChild(makeInventorySection("📦 库存数据", data.inventory)); content.appendChild(makecontractsOutgoingSection("📦 出库合同", data.contractsOutgoing)); content.appendChild(makeContractsIncomingSection("📦 入库合同", data.contractsIncoming)); content.appendChild(makeMarketSection("📦 市场订单", data.market)); }; function makeInventorySection(label, items) { const containerDiv = document.createElement("div"); if (items.length === 0) { const msg = document.createElement("div"); msg.textContent = "暂无数据。"; msg.style.padding = "5px 10px"; containerDiv.appendChild(msg); return createToggleSection(label, containerDiv, false); } const groupedByKind = {}; items.forEach(item => { if (!groupedByKind[item.kind]) groupedByKind[item.kind] = []; groupedByKind[item.kind].push(item); }); for (const kind in groupedByKind) { const kindName = KIND_NAMES[kind] || `种类 ${kind}`; const kindContent = document.createElement("div"); kindContent.style.paddingLeft = "12px"; const groupedByQuality = {}; groupedByKind[kind].forEach(item => { if (!groupedByQuality[item.quality]) groupedByQuality[item.quality] = []; groupedByQuality[item.quality].push(item); }); for (const quality in groupedByQuality) { const qualityContent = document.createElement("div"); qualityContent.style.paddingLeft = "16px"; const headerRow = document.createElement('div'); headerRow.style.fontWeight = 'bold'; headerRow.style.display = 'flex'; headerRow.style.gap = '16px'; headerRow.style.padding = '2px 0'; headerRow.innerHTML = `<div style="width:100px">剩余量</div><div style="width:130px">达成时间</div><div style="width:80px">单位成本</div>`; qualityContent.appendChild(headerRow); const allDecayArrays = groupedByQuality[quality].flatMap(i => i.futureDecayArray || i.result || []); if (allDecayArrays.length === 0) { const row = document.createElement("div"); row.style.display = "flex"; row.style.gap = "16px"; row.style.padding = "1px 0"; row.innerHTML = ` <div style="width:100px">已全部衰减</div> <div style="width:130px">-</div> <div style="width:80px">∞</div> `; qualityContent.appendChild(row); } else { allDecayArrays.forEach(({ amount, time, unitCost }) => { const row = document.createElement("div"); row.style.display = "flex"; row.style.gap = "16px"; row.style.padding = "1px 0"; row.innerHTML = ` <div style="width:100px">${amount}</div> <div style="width:130px">${time}</div> <div style="width:80px">${unitCost === Infinity ? '∞' : (typeof unitCost === 'number' ? unitCost.toFixed(3) : '∞') }</div> `; qualityContent.appendChild(row); }); } kindContent.appendChild(createToggleSection(`品质 ${quality}`, qualityContent, false)); } containerDiv.appendChild(createToggleSection(kindName, kindContent, true)); } return createToggleSection(label, containerDiv, true); } function makecontractsOutgoingSection(label, contractsData) { const container = document.createElement("div"); if (!contractsData || Object.keys(contractsData).length === 0) { const msg = document.createElement("div"); msg.textContent = "暂无数据。"; msg.style.padding = "5px 10px"; container.appendChild(msg); return createToggleSection(label, container, false); } for (const kind in contractsData) { const kindName = KIND_NAMES[kind] || `种类 ${kind}`; const kindContent = document.createElement("div"); kindContent.style.paddingLeft = "12px"; for (const buyer in contractsData[kind]) { const buyerContent = document.createElement("div"); buyerContent.style.paddingLeft = "16px"; const sortedContracts = contractsData[kind][buyer].slice().sort((a, b) => { return Date.parse(a.datetime) - Date.parse(b.datetime); }); sortedContracts.forEach((contract, idx) => { const contractContent = document.createElement("div"); contractContent.style.paddingLeft = "16px"; contractContent.style.marginBottom = "4px"; const headerRow = document.createElement('div'); headerRow.style.fontWeight = 'bold'; headerRow.style.display = 'flex'; headerRow.style.gap = '12px'; headerRow.style.padding = '2px 0'; headerRow.innerHTML = ` <div style="width:100px">剩余量</div> <div style="width:150px">达成时间</div> `; contractContent.appendChild(headerRow); if (!contract.result || contract.result.length === 0) { const row = document.createElement("div"); row.textContent = "已全部衰减"; row.style.padding = "2px 0 2px 10px"; contractContent.appendChild(row); } else { contract.result.forEach(({ amount, time }) => { const row = document.createElement("div"); row.style.display = "flex"; row.style.gap = "12px"; row.style.padding = "1px 0"; row.innerHTML = ` <div style="width:100px">${amount}</div> <div style="width:150px">${time}</div> `; contractContent.appendChild(row); }); } buyerContent.appendChild(createToggleSection( `品质 Q${contract.quality}|数量 ${contract.quantity}|单价 $${contract.price}|发出 ${new Date(contract.datetime).toLocaleString(undefined, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', })}`, contractContent, false )); }); kindContent.appendChild(createToggleSection(`买方公司 ${buyer}`, buyerContent, true)); } container.appendChild(createToggleSection(kindName, kindContent, true)); } return createToggleSection(label, container, true); } function makeContractsIncomingSection(label, contractsData) { const container = document.createElement("div"); if (!contractsData || Object.keys(contractsData).length === 0) { const msg = document.createElement("div"); msg.textContent = "暂无数据。"; msg.style.padding = "5px 10px"; container.appendChild(msg); return createToggleSection(label, container, false); } for (const kind in contractsData) { const kindName = KIND_NAMES[kind] || `种类 ${kind}`; const kindContent = document.createElement("div"); kindContent.style.paddingLeft = "12px"; for (const seller in contractsData[kind]) { const sellerContent = document.createElement("div"); sellerContent.style.paddingLeft = "16px"; const sortedContracts = contractsData[kind][seller].slice().sort((a, b) => { return Date.parse(a.datetime) - Date.parse(b.datetime); }); sortedContracts.forEach((contract, idx) => { const contractContent = document.createElement("div"); contractContent.style.paddingLeft = "16px"; contractContent.style.marginBottom = "4px"; const headerRow = document.createElement('div'); headerRow.style.fontWeight = 'bold'; headerRow.style.display = 'flex'; headerRow.style.gap = '12px'; headerRow.style.padding = '2px 0'; headerRow.innerHTML = ` <div style="width:100px">剩余量</div> <div style="width:150px">达成时间</div> `; contractContent.appendChild(headerRow); if (!contract.result || contract.result.length === 0) { const row = document.createElement("div"); row.textContent = "已全部衰减"; row.style.padding = "2px 0 2px 10px"; contractContent.appendChild(row); } else { contract.result.forEach(({ amount, time }) => { const row = document.createElement("div"); row.style.display = "flex"; row.style.gap = "12px"; row.style.padding = "1px 0"; row.innerHTML = ` <div style="width:100px">${amount}</div> <div style="width:150px">${time}</div> `; contractContent.appendChild(row); }); } sellerContent.appendChild(createToggleSection( `品质 Q${contract.quality}|数量 ${contract.quantity}|单价 $${contract.price}|发出 ${new Date(contract.datetime).toLocaleString(undefined, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', })}`, contractContent, false )); }); kindContent.appendChild(createToggleSection(`卖方公司 ${seller}`, sellerContent, true)); } container.appendChild(createToggleSection(kindName, kindContent, true)); } return createToggleSection(label, container, true); } function makeMarketSection(label, items) { const containerDiv = document.createElement("div"); if (items.length === 0) { const msg = document.createElement("div"); msg.textContent = "暂无数据。"; msg.style.padding = "5px 10px"; containerDiv.appendChild(msg); return createToggleSection(label, containerDiv, false); } const groupedByKind = {}; items.forEach(item => { if (!groupedByKind[item.kind]) groupedByKind[item.kind] = []; groupedByKind[item.kind].push(item); }); for (const kind in groupedByKind) { const kindName = KIND_NAMES[kind] || `种类 ${kind}`; const kindContent = document.createElement("div"); kindContent.style.paddingLeft = "12px"; const groupedByQuality = {}; groupedByKind[kind].forEach(item => { if (!groupedByQuality[item.quality]) groupedByQuality[item.quality] = []; groupedByQuality[item.quality].push(item); }); for (const quality in groupedByQuality) { const qualityContent = document.createElement("div"); qualityContent.style.paddingLeft = "16px"; const groupedByPrice = {}; groupedByQuality[quality].forEach(item => { if (!groupedByPrice[item.price]) groupedByPrice[item.price] = []; groupedByPrice[item.price].push(item); }); for (const price in groupedByPrice) { const priceContent = document.createElement("div"); priceContent.style.paddingLeft = "16px"; const headerRow = document.createElement('div'); headerRow.style.fontWeight = 'bold'; headerRow.style.display = 'flex'; headerRow.style.gap = '16px'; headerRow.style.padding = '2px 0'; headerRow.innerHTML = `<div style="width:100px">剩余量</div><div style="width:130px">达成时间</div>`; priceContent.appendChild(headerRow); const allDecayArrays = groupedByPrice[price].flatMap(i => i.result || []); if (allDecayArrays.length === 0) { const row = document.createElement("div"); row.style.display = "flex"; row.style.gap = "16px"; row.style.padding = "1px 0"; row.innerHTML = ` <div style="width:100px">已全部衰减</div> <div style="width:130px">-</div> `; priceContent.appendChild(row); } else { allDecayArrays.forEach(({ amount, time }) => { const row = document.createElement("div"); row.style.display = "flex"; row.style.gap = "16px"; row.style.padding = "1px 0"; row.innerHTML = ` <div style="width:100px">${amount}</div> <div style="width:130px">${time}</div> `; priceContent.appendChild(row); }); } qualityContent.appendChild(createToggleSection(`单价 $${price}`, priceContent, false)); } kindContent.appendChild(createToggleSection(`品质 ${quality}`, qualityContent, false)); } containerDiv.appendChild(createToggleSection(kindName, kindContent, true)); } return createToggleSection(label, containerDiv, true); } const init = () => { const isMobile = /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent); let resizer; container = document.createElement("div"); container.id = 'decayDataPanel'; container.style.cssText = ` position: fixed; left: ${isMobile ? '5vw' : 'calc(100% - 510px)'}; top: ${isMobile ? '20px' : 'calc(100vh - 60px - 300px)'}; width: ${isMobile ? '80vw' : '500px'}; height: ${isMobile ? '50vh' : '350px'}; max-height: 80%; overflow: hidden; background: #222; color: white; padding: 10px; z-index: 9998; border-radius: 6px; font-size: clamp(12px, 1.5vw, 16px); box-shadow: 0 0 10px #000; user-select: none; display: flex; flex-direction: column; `; // 标题栏:拖动区域 header = document.createElement('div'); const headerTitle = document.createElement('span'); headerTitle.textContent = '未来衰减量 ▾'; header.appendChild(headerTitle); // 折叠逻辑 let isCollapsed = false; let lastKnownHeight = isMobile ? '50vh' : '350px'; header.addEventListener('click', (e) => { if (e.target === calcBtn || e.target === closeBtn) return; isCollapsed = !isCollapsed; if (isCollapsed) { content.style.display = 'none'; container.style.height = `${header.offsetHeight + 2}px`; if (resizer) resizer.style.display = 'none'; } else { content.style.display = 'block'; container.style.height = lastKnownHeight; if (resizer) resizer.style.display = 'block'; content.style.height = `calc(100% - ${header.offsetHeight}px)`; } headerTitle.textContent = isCollapsed ? '未来衰减量 ▸' : '未来衰减量 ▾'; }); header.style.cssText = ` background: #444; padding: 8px 10px; font-weight: bold; border-top-left-radius: 6px; border-top-right-radius: 6px; flex-shrink: 0; position: relative; ${isMobile ? '' : 'cursor: move;'} `; const calcBtn = document.createElement('button'); calcBtn.textContent = '🔄'; calcBtn.title = '重新计算资源剩余量'; calcBtn.style.cssText = ` float: right; margin-right: 6px; background: transparent; border: none; color: white; font-size: 16px; cursor: pointer; user-select: none; `; calcBtn.onclick = async () => { calcBtn.disabled = true; calcBtn.textContent = '⏳'; try { await window.calculateAll(); DecayResultViewer.show(); } catch (e) { console.error("资源计算失败", e); } finally { calcBtn.disabled = false; calcBtn.textContent = '🔄'; } }; header.appendChild(calcBtn); const closeBtn = document.createElement('button'); closeBtn.textContent = '×'; closeBtn.title = '关闭面板'; closeBtn.style.cssText = ` position: absolute; right: 8px; top: 6px; background: transparent; border: none; color: white; font-size: 16px; cursor: pointer; user-select: none; `; closeBtn.onclick = () => { container.style.display = 'none'; }; header.appendChild(closeBtn); content = document.createElement('div'); content.style.cssText = ` flex: 1 1 auto; overflow: auto; padding: 10px; `; container.appendChild(header); container.appendChild(content); document.body.appendChild(container); renderResult(); if (!isMobile) { let isDragging = false, startX, startY, startLeft, startTop; header.addEventListener('mousedown', (e) => { if (e.target === closeBtn) return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = container.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; e.preventDefault(); }); window.addEventListener('mouseup', () => { isDragging = false; }); window.addEventListener('mousemove', (e) => { if (!isDragging) return; let newLeft = startLeft + (e.clientX - startX); let newTop = startTop + (e.clientY - startY); newLeft = Math.min(Math.max(newLeft, 0), window.innerWidth - container.offsetWidth); newTop = Math.min(Math.max(newTop, 0), window.innerHeight - container.offsetHeight); container.style.left = newLeft + 'px'; container.style.top = newTop + 'px'; container.style.bottom = 'auto'; }); resizer = document.createElement('div'); resizer.style.cssText = ` width: 14px; height: 14px; background: transparent; position: absolute; right: 2px; bottom: 2px; cursor: se-resize; user-select: none; z-index: 9998; `; container.appendChild(resizer); let isResizing = false; let startWidth, startHeight, startPageX, startPageY; resizer.addEventListener('mousedown', (e) => { isResizing = true; startWidth = container.offsetWidth; startHeight = container.offsetHeight; startPageX = e.pageX; startPageY = e.pageY; e.preventDefault(); e.stopPropagation(); }); window.addEventListener('mousemove', (e) => { if (!isResizing) return; let newWidth = startWidth + (e.pageX - startPageX); let newHeight = startHeight + (e.pageY - startPageY); newWidth = Math.max(newWidth, 250); newHeight = Math.max(newHeight, 150); newWidth = Math.min(newWidth, window.innerWidth - container.getBoundingClientRect().left); newHeight = Math.min(newHeight, window.innerHeight - container.getBoundingClientRect().top); container.style.width = newWidth + 'px'; container.style.height = newHeight + 'px'; content.style.height = `calc(100% - ${header.offsetHeight}px)`; }); window.addEventListener('mouseup', () => { if (isResizing) { lastKnownHeight = container.style.height; isResizing = false; } }); } if (isMobile) { let isDragging = false, startX, startY, startLeft, startTop; header.addEventListener('touchstart', (e) => { if (e.target === closeBtn) return; const touch = e.touches[0]; isDragging = true; startX = touch.clientX; startY = touch.clientY; const rect = container.getBoundingClientRect(); startLeft = rect.left; startTop = rect.top; }, { passive: true }); window.addEventListener('touchend', () => { isDragging = false; }); window.addEventListener('touchmove', (e) => { if (!isDragging) return; const touch = e.touches[0]; let newLeft = startLeft + (touch.clientX - startX); let newTop = startTop + (touch.clientY - startY); newLeft = Math.min(Math.max(newLeft, 0), window.innerWidth - container.offsetWidth); newTop = Math.min(Math.max(newTop, 0), window.innerHeight - container.offsetHeight); container.style.left = newLeft + 'px'; container.style.top = newTop + 'px'; container.style.bottom = 'auto'; }, { passive: true }); } }; window.addEventListener('warehouse-updated', () => { if (container && container.style.display !== 'none') { renderResult(); } }); window.addEventListener('marketOrders-updated', () => { if (container && container.style.display !== 'none') { renderResult(); } }); window.addEventListener('contractsOutgoing-updated', () => { if (container && container.style.display !== 'none') { renderResult(); } }); window.addEventListener('contractsIncoming-updated', () => { if (container && container.style.display !== 'none') { renderResult(); } }); return { show() { if (!container) init(); else container.style.display = "flex"; renderResult(); }, hide() { if (container) container.style.display = "none"; }, toggle() { if (!container || container.style.display === "none") this.show(); else this.hide(); } }; })(); // ====================== // 模块13:计算MP-?% // ====================== (function () { let cachedRetailIds = null; function getRetailIds() { if (cachedRetailIds) return cachedRetailIds; const SCDStr = localStorage.getItem("SimcompaniesConstantsData"); if (!SCDStr) return new Set(); try { const SCD = JSON.parse(SCDStr); if (!SCD.data || !SCD.data.SALES) return new Set(); const sales = SCD.data.SALES; const retailIds = new Set(); Object.keys(sales).forEach(key => { const arr = sales[key]; if (Array.isArray(arr)) arr.forEach(id => retailIds.add(id)); }); cachedRetailIds = retailIds; return retailIds; } catch { return new Set(); } } function isRetailId(id) { const retailIds = getRetailIds(); return retailIds.has(id); } // 1. 创建Worker的函数,返回一个对象包含postMessage方法等 function createProfitWorker() { const workerCode = ` self.onmessage = function(e) { const { data, inputPercent, SCD, SRC } = e.data; // bring constants into worker scope const lwe = SCD.retailInfo; const zn = SCD.data; // Utility functions defined inside to use local lwe and zn const Ul = (overhead, skillCOO) => { const r = overhead || 1; return r - (r - 1) * skillCOO / 100; }; const wv = (e, t, r) => { return r === null ? lwe[e][t] : lwe[e][t].quality[r]; }; const Upt = (e, t, r, n) => t + (e + n) / r; const Hpt = (e, t, r, n, a) => { const o = (n + e) / ((t - a) * (t - a)); return e - (r - t) * (r - t) * o; }; const qpt = (e, t, r, n, a = 1) => (a * ((n - t) * 3600) - r) / (e + r); const Bpt = (e, t, r, n, a, o) => { const g = zn.RETAIL_ADJUSTMENT[e] ?? 1; const s = Math.min(Math.max(2 - n, 0), 2), l = Math.max(0.9, s / 2 + 0.5), c = r / 12; const d = zn.PROFIT_PER_BUILDING_LEVEL * (t.buildingLevelsNeededPerUnitPerHour * t.modeledUnitsSoldAnHour + 1) * g * (s / 2 * (1 + c * zn.RETAIL_MODELING_QUALITY_WEIGHT)) + (t.modeledStoreWages ?? 0); const h = t.modeledUnitsSoldAnHour * l; const p = Upt(d, t.modeledProductionCostPerUnit, h, t.modeledStoreWages ?? 0); const m = Hpt(d, p, o, t.modeledStoreWages ?? 0, t.modeledProductionCostPerUnit); return qpt(m, t.modeledProductionCostPerUnit, t.modeledStoreWages ?? 0, o, a); }; const zL = (buildingKind, modeledData, quantity, salesModifier, price, qOverride, saturation, acc, size, weather) => { const u = Bpt(buildingKind, modeledData, qOverride, saturation, quantity, price); if (u <= 0) return NaN; const d = u / acc / size; let p = d - d * salesModifier / 100; return weather && (p /= weather.sellingSpeedMultiplier), p }; // Initial debug log const results = data.map(order => { // profit calculation loop let currentPrice = inputPercent < 0 ? order.price + inputPercent : order.price * (1 - inputPercent/100), cost = currentPrice, quantity = order.quantity, maxProfit = -Infinity, size = 1, acceleration = SRC.acceleration, economyState = SRC.economyState, salesModifierWithRecreationBonus = SRC.salesModifier + SRC.recreationBonus, skillCMO = SRC.saleBonus, skillCOO = SRC.adminBonus; if(order.kind === 153 || order.kind === 154){ quantity = Math.floor(order.quantity * Math.pow(1 - 0.05, (Math.round((Math.abs(Date.now() - Date.parse(order.datetimeDecayUpdated))) / (1000 * 60) / 4) * 4 / 60))) } // compute saturation locally const saturation = (() => { const list = SRC.ResourcesRetailInfo; const m = list.find(item => item.dbLetter === parseInt(order.kind) && (parseInt(order.kind) !== 150 || item.quality === order.quality) ); return m?.saturation; })(); const administrationOverhead = SRC.administration; const buildingKind = Object.entries(zn.SALES).find(([k, ids]) => ids.includes(parseInt(order.kind)) )?.[0]; const salaryModifier = SCD.buildingsSalaryModifier?.[buildingKind]; const averageSalary = zn.AVERAGE_SALARY; const wages = averageSalary * salaryModifier; const forceQuality = (parseInt(order.kind) === 150) ? order.quality : undefined; const resourceDetail = SCD.constantsResources[parseInt(order.kind)] const v = salesModifierWithRecreationBonus + skillCMO; const b = Ul(administrationOverhead, skillCOO); let selltime; while (currentPrice > 0) { const modeledData = wv(economyState, order.kind, forceQuality ?? null); const w = zL( buildingKind, modeledData, quantity, v, currentPrice, forceQuality === void 0 ? order.quality : 0, saturation, acceleration, size, resourceDetail.retailSeason === "Summer" ? SRC.sellingSpeedMultiplier : void 0 ); const revenue = currentPrice * quantity; const wagesTotal = Math.ceil(w * wages * acceleration * b / 3600); const secondsToFinish = w; const profit = (!secondsToFinish || secondsToFinish <= 0) ? NaN : (revenue - cost * quantity - wagesTotal) / secondsToFinish; if (!secondsToFinish || secondsToFinish <= 0) break; if (profit > maxProfit) { maxProfit = profit; selltime = secondsToFinish; } else if (maxProfit > 0 && profit < 0) { break; } // price increment if (currentPrice < 8) { currentPrice = Math.round((currentPrice + 0.01) * 100) / 100; } else if (currentPrice < 2001) { currentPrice = Math.round((currentPrice + 0.1) * 10) / 10; } else { currentPrice = Math.round(currentPrice + 1); } } // 返回每个订单的计算结果 return { seller: order.seller?.company || "", marketPrice: order.price, quality: order.quality, saleAmout: quantity, contractPrice: cost, contractMaxProfit: (maxProfit * 3600).toFixed(2) }; }); self.postMessage(results); }; `; const worker = new Worker(URL.createObjectURL(new Blob([workerCode], { type: 'application/javascript' }))); return worker; } // 2. 实例化 Worker 并暴露接口,方便 MpPanel 调用 const profitWorker = createProfitWorker(); window.MarketInterceptor = { profitWorker, calculateProfit(inputPercent, data, realmId) { const SCD = JSON.parse(localStorage.getItem("SimcompaniesConstantsData")); const SRC = JSON.parse(localStorage.getItem(`SimcompaniesRetailCalculation_${realmId}`)); return new Promise((resolve) => { profitWorker.onmessage = (e) => { resolve(e.data); }; profitWorker.postMessage({ data, inputPercent, SCD, SRC }); }); } }; // 3. processMarketData function processMarketData(json, realm, id) { if (!Array.isArray(json)) return; localStorage.setItem(`market_${realm}_${id}`, JSON.stringify(json)); } const originalFetch = window.fetch; window.fetch = async function (...args) { const url = args[0]; const match = typeof url === 'string' && url.match(/\/api\/v3\/market\/(\d+)\/(\d+)\/?($|\?)/); if (match) { const realm = parseInt(match[1], 10); const id = parseInt(match[2], 10); if (!isRetailId(id)) return originalFetch(...args); const response = await originalFetch(...args); response.clone().json().then(json => { processMarketData(json, realm, id); }).catch(() => { }); return response; } return originalFetch(...args); }; const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._isTargetMarketRequest = false; try { const match = typeof url === 'string' && url.match(/\/api\/v3\/market\/(\d+)\/(\d+)(\/|$|\?)/); if (match) { const realm = parseInt(match[1], 10); const id = parseInt(match[2], 10); if (isRetailId(id)) { this._isTargetMarketRequest = true; this._realm = realm; this._id = id; this.addEventListener('readystatechange', () => { if (this.readyState === 4 && this.status >= 200 && this.status < 300) { try { const json = JSON.parse(this.responseText); processMarketData(json, this._realm, this._id); } catch { } } }, false); } } } catch { } return originalOpen.call(this, method, url, ...rest); }; XMLHttpRequest.prototype.send = function (...args) { return originalSend.call(this, ...args); }; })(); // ====================== // 检测更新 // ====================== function compareVersions(v1, v2) { const a = v1.split('.').map(Number); const b = v2.split('.').map(Number); const len = Math.max(a.length, b.length); for (let i = 0; i < len; i++) { const num1 = a[i] || 0; const num2 = b[i] || 0; if (num1 > num2) return 1; if (num1 < num2) return -1; } return 0; } function checkUpdate() { const localVersion = GM_info.script.version; const scriptUrl = 'https://simcompanies-scripts.pages.dev/autoMaxPPHPL.user.js?t=' + Date.now(); const downloadUrl = 'https://simcompanies-scripts.pages.dev/autoMaxPPHPL.user.js'; // @changelog 同步最新计算公式 l = Math.max(.9, s / 2 + .5) fetch(scriptUrl) .then(res => { if (!res.ok) throw new Error('获取失败'); return res.text(); }) .then(remoteText => { const matchVersion = remoteText.match(/^\s*\/\/\s*@version\s+([0-9.]+)/m); const matchChange = remoteText.match(/^\s*\/\/\s*@changelog\s+(.+)/m); if (!matchVersion) return; const latestVersion = matchVersion[1]; const changeLog = matchChange ? matchChange[1] : ''; if (compareVersions(latestVersion, localVersion) > 0) { console.log(`📢 检测到新版本 v${latestVersion}`); if (confirm(`自动计算最大时利润插件检测到新版本 v${latestVersion},是否前往更新?\n\nv${latestVersion} ${changeLog}\n\n关于版本号说明 1.X.Y ,X为增添新功能或修复不可用,Y为细节修改不影响功能,如不需更新可将Y或其它位置修改为较大值。`)) { window.open(downloadUrl, '_blank'); } } else { console.log("✅ 当前已是最新版本"); } }) .catch(err => { console.warn('检查更新失败:', err); }); } setTimeout(checkUpdate, 3000); })();