MWI-Equipment-Diff

Make life easier

  1. // ==UserScript==
  2. // @name MWI-Equipment-Diff
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.7
  5. // @description Make life easier
  6. // @author BKN46
  7. // @match https://*.milkywayidle.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12. (function() {
  13. 'use strict';
  14. let selfData = {};
  15. const colors = {
  16. info: 'rgb(0, 108, 158)',
  17. smaller: 'rgb(199, 21, 21)',
  18. greater: 'rgb(23, 151, 12)',
  19. };
  20. let equipmentToDiff = {};
  21. const isZHInGameSetting = localStorage.getItem("i18nextLng")?.toLowerCase()?.startsWith("zh");
  22. let isZH = isZHInGameSetting;
  23. function transZH(zh) {
  24. if (isZH) return zh;
  25. return {
  26. "战斗风格": "Combat Style",
  27. "伤害类型": "Damage Type",
  28. "自动攻击伤害": "Auto Attack Damage",
  29. "攻击速度": "Attack Speed",
  30. "攻击间隔": "Attack Interval",
  31. "暴击率": "Critical Rate",
  32. "暴击伤害": "Critical Damage",
  33. "技能急速": "Ability Haste",
  34. "施法速度": "Cast Speed",
  35. "刺击精准度": "Stab Accuracy",
  36. "刺击伤害": "Stab Damage",
  37. "斩击精准度": "Slash Accuracy",
  38. "斩击伤害": "Slash Damage",
  39. "钝击精准度": "Smash Accuracy",
  40. "钝击伤害": "Smash Damage",
  41. "远程精准度": "Ranged Accuracy",
  42. "远程伤害": "Ranged Damage",
  43. "魔法精准度": "Magic Accuracy",
  44. "魔法伤害": "Magic Damage",
  45. "物理增幅": "Physical Amplify",
  46. "火系增幅": "Fire Amplify",
  47. "水系增幅": "Water Amplify",
  48. "自然系增幅": "Nature Amplify",
  49. "护甲穿透": "Armor Penetration",
  50. "火系穿透": "Fire Penetration",
  51. "水系穿透": "Water Penetration",
  52. "自然系穿透": "Nature Penetration",
  53. }[zh] || zh;
  54. }
  55. function parseEquipmentModal(element) {
  56. const equipmentDetail = {};
  57. const detailLines = element.querySelectorAll('.EquipmentStatsText_stat__27Sus');
  58. for (const line of detailLines) {
  59. if (line.querySelector('.EquipmentStatsText_uniqueStat__2xvqX')) continue;
  60. const data = line.textContent.split(':');
  61. if (data.length === 2) {
  62. const key = data[0].trim();
  63. const value = data[1].split('(')[0].trim();
  64. if (value === 'N/A') continue;
  65. equipmentDetail[key] = value;
  66. }
  67. }
  68. if (!element.querySelector('.diff-tooltip')) {
  69. const diffTooltip = document.createElement('div');
  70. diffTooltip.className = 'diff-tooltip';
  71. diffTooltip.textContent = isZH ? '按\'d\'来添加作为对比对象' : 'Press "d" to add to quick diff';
  72. diffTooltip.style = `color: ${colors.info}; font-weight: bold;`;
  73. element.appendChild(diffTooltip);
  74. if (equipmentToDiff.data) {
  75. const diffRemoveTooltip = document.createElement('div');
  76. diffRemoveTooltip.className = 'diff-remove-tooltip';
  77. diffRemoveTooltip.textContent = isZH ? '按\'r\'来移除当前对比' : 'Press "r" to remove present quick diff';
  78. diffRemoveTooltip.style = `color: ${colors.info}; font-weight: bold;`;
  79. element.appendChild(diffRemoveTooltip);
  80. }
  81. }
  82. return equipmentDetail;
  83. }
  84. // #region Diff Modal
  85. function addDiffToModal(element, data, price) {
  86. if (element.querySelector('.diff-value') || element.querySelector('.diff-header')) return;
  87. const parentArea = element.querySelector('.EquipmentStatsText_equipmentStatsText__djKBS');
  88. const TextArea = parentArea.firstChild;
  89. const diffMap = {};
  90. const detailLines = element.querySelectorAll('.EquipmentStatsText_stat__27Sus');
  91. for (const line of detailLines) {
  92. const key = line.textContent.split(':')[0].trim();
  93. const valueElement = line.querySelectorAll('span')[1];
  94. const diffSpan = document.createElement('span');
  95. diffSpan.className = 'diff-value';
  96. if (key in equipmentToDiff.data) {
  97. const diffValue = equipmentToDiff.data[key];
  98. if (data[key] === diffValue) {
  99. continue;
  100. }
  101. const diff = strDiff(diffValue, data[key]);
  102. diffMap[key] = diff;
  103. diffSpan.textContent = ` (${diff})`;
  104. let color = colors.info;
  105. if (data[key].endsWith('s')) {
  106. color = diff.startsWith('-') ? colors.greater : colors.smaller;
  107. } else {
  108. color = diff.startsWith('-') ? colors.smaller : colors.greater;
  109. }
  110. diffSpan.style = `color: ${color}; font-weight: bold;`;
  111. valueElement.appendChild(diffSpan);
  112. } else if (valueElement) {
  113. diffSpan.textContent = ` (${valueElement.textContent})`;
  114. // diffMap[key] = valueElement.textContent.startsWith('-') ? valueElement.textContent.replace('-', '') : `-${valueElement.textContent}`;
  115. diffMap[key] = valueElement.textContent;
  116. const color = valueElement.textContent.startsWith('-') ? colors.smaller: colors.greater;
  117. diffSpan.style = `color: ${color}; font-weight: bold;`;
  118. valueElement.appendChild(diffSpan);
  119. } else { // special ability on equipment
  120. // diffSpan.textContent = ` (N/A)`;
  121. // diffSpan.style = `color: ${colors.smaller}; font-weight: bold;`;
  122. // line.appendChild(diffSpan);
  123. }
  124. }
  125. for (const key in equipmentToDiff.data) {
  126. if (!(key in data)) {
  127. const newLine = document.createElement('div');
  128. newLine.className = 'EquipmentStatsText_stat__27Sus';
  129. const keySpan = document.createElement('span');
  130. keySpan.textContent = `${key}: `;
  131. newLine.appendChild(keySpan);
  132. const valueSpan = document.createElement('span');
  133. valueSpan.textContent = 'N/A';
  134. newLine.appendChild(valueSpan);
  135. const diffSpan = document.createElement('span');
  136. diffSpan.className = 'diff-value';
  137. let diffValue = '';
  138. if (equipmentToDiff.data[key].startsWith('-')) {
  139. diffValue = equipmentToDiff.data[key].replace('-', '+');
  140. } else if (equipmentToDiff.data[key].startsWith('+')) {
  141. diffValue = equipmentToDiff.data[key].replace('+', '-');
  142. } else {
  143. diffValue = `-${equipmentToDiff.data[key]}`;
  144. }
  145. diffSpan.textContent = ` (${diffValue})`;
  146. diffMap[key] = diffValue;
  147. const color = equipmentToDiff.data[key].startsWith('-') ? colors.greater : colors.smaller;
  148. diffSpan.style = `color: ${color}; font-weight: bold;`;
  149. valueSpan.appendChild(diffSpan);
  150. TextArea.appendChild(newLine);
  151. }
  152. }
  153. // #region Diff header
  154. if (equipmentToDiff.name) {
  155. const newLine = document.createElement('div');
  156. newLine.className = 'diff-header';
  157. newLine.style = 'display: flex; grid-gap: 6px; gap: 6px; justify-content: space-between;'
  158. const keySpan = document.createElement('span');
  159. keySpan.textContent = isZH ? '对比对象: ' : 'Compares to: ';
  160. keySpan.style = `color: ${colors.info}; font-weight: bold;`;
  161. newLine.appendChild(keySpan);
  162. const diffTitle = document.createElement('span');
  163. diffTitle.textContent = equipmentToDiff.name;
  164. diffTitle.style = `color: ${colors.info}; font-weight: bold;`;
  165. newLine.appendChild(diffTitle);
  166. parentArea.insertBefore(newLine, TextArea);
  167. }
  168. if (price >= 0 && equipmentToDiff.price >= 0) {
  169. const newLine = document.createElement('div');
  170. newLine.className = 'diff-header';
  171. newLine.style = 'display: flex; grid-gap: 6px; gap: 6px; justify-content: space-between;'
  172. const keySpan = document.createElement('span');
  173. keySpan.textContent = isZH ? `价格差: ` : `Price diff: `;
  174. keySpan.style = `color: ${colors.info}; font-weight: bold;`;
  175. newLine.appendChild(keySpan);
  176. const priceSpan = document.createElement('span');
  177. priceSpan.className = 'diff-price';
  178. const priceDiff = priceParse(price - equipmentToDiff.price);
  179. priceSpan.textContent = `${priceDiff}`;
  180. const priceColor = priceDiff.startsWith('-') ? colors.greater : colors.smaller;
  181. priceSpan.style = `color: ${priceColor}; font-weight: bold;`;
  182. newLine.appendChild(priceSpan);
  183. parentArea.insertBefore(newLine, TextArea);
  184. }
  185. // #region DPS diff
  186. if (Object.keys(diffMap).length > 0) {
  187. const newLine = document.createElement('div');
  188. newLine.className = 'diff-header';
  189. newLine.style = 'display: flex; grid-gap: 6px; gap: 6px; justify-content: space-between;'
  190. const keySpan = document.createElement('span');
  191. keySpan.textContent = isZH ? `粗略DPS变化: ` : `Rough DPS diff: `;
  192. keySpan.style = `color: ${colors.info}; font-weight: bold;`;
  193. newLine.appendChild(keySpan);
  194. const selfType = getSelfStyleTrans();
  195. const originStyle = equipmentToDiff.data[transZH('战斗风格')];
  196. const targetStyle = data[transZH('战斗风格')];
  197. if ((originStyle || targetStyle) && (targetStyle != selfType || originStyle != selfType)) {
  198. const diffValue = document.createElement('span');
  199. diffValue.textContent = isZH ? '[战斗风格不一致]' : '[Different combat style]';
  200. diffValue.style = `color: ${colors.info}; font-weight: bold;`;
  201. newLine.appendChild(diffValue);
  202. parentArea.insertBefore(newLine, TextArea);
  203. return;
  204. }
  205. let dpsCompare = 0;
  206. try {
  207. dpsCompare = getDPSCompare(diffMap);
  208. const diffValue = document.createElement('span');
  209. diffValue.textContent = dpsCompare * 100 > 0 ? `+${(dpsCompare * 100).toFixed(3)}%` : `${(dpsCompare * 100).toFixed(3)}%`;
  210. const color = dpsCompare > 0 ? colors.greater : colors.smaller;
  211. diffValue.style = `color: ${color}; font-weight: bold;`;
  212. newLine.appendChild(diffValue);
  213. } catch (error) {
  214. const diffValue = document.createElement('span');
  215. diffValue.textContent = isZH ? '[计算过程发生错误]' : '[Error]';
  216. diffValue.style = `color: ${colors.info}; font-weight: bold;`;
  217. console.error('Equipment Diff: Error calculating DPS diff:', error);
  218. newLine.appendChild(diffValue);
  219. }
  220. parentArea.insertBefore(newLine, TextArea);
  221. if (dpsCompare > 0 && price >= 0 && equipmentToDiff.price >= 0 && (price - equipmentToDiff.price) > 0) {
  222. const priceLine = document.createElement('div');
  223. priceLine.className = 'diff-header';
  224. priceLine.style = 'display: flex; grid-gap: 6px; gap: 6px; justify-content: space-between;'
  225. const priceKeySpan = document.createElement('span');
  226. priceKeySpan.textContent = isZH ? `每10M价格DPS提升: ` : `DPS% per 10M coin: `;
  227. priceKeySpan.style = `color: ${colors.info}; font-weight: bold;`;
  228. priceLine.appendChild(priceKeySpan);
  229. const dpsPriceSpan = document.createElement('span');
  230. dpsPriceSpan.className = 'diff-price';
  231. const dpsPerMil = (dpsCompare * 100) / ((price - equipmentToDiff.price) / 1e6) * 10;
  232. dpsPriceSpan.textContent = dpsPerMil > 0 ? `+${dpsPerMil.toFixed(5)}%` : `${dpsPerMil.toFixed(5)}%`;
  233. const priceColor = dpsPerMil > 0 ? colors.greater : colors.smaller;
  234. dpsPriceSpan.style = `color: ${priceColor}; font-weight: bold;`;
  235. priceLine.appendChild(dpsPriceSpan);
  236. parentArea.insertBefore(priceLine, TextArea);
  237. }
  238. }
  239. }
  240. function strDiff(s1, s2) {
  241. if (s1 === s2) return '';
  242. if (!s1 || !s2) return s1 || s2;
  243. let postfix = '';
  244. let isNeg = false;
  245. if (s1.endsWith('%')) postfix = '%';
  246. if (s1.startsWith('-')) isNeg = true;
  247. const proc = (t) => parseFloat(t.replace('%', '').replace(',', '').replace(' ', '').replace('+', '').replace('_', ''));
  248. const diff = proc(s2) - proc(s1);
  249. if (isNaN(diff)) return s1;
  250. if (diff === 0) return '';
  251. if (diff > 0) return `${isNeg ? '-' : '+'}${parseFloat(diff.toFixed(3))}${postfix}`;
  252. return `${isNeg ? '+' : '-'}${parseFloat(Math.abs(diff).toFixed(3))}${postfix}`;
  253. }
  254. function hookWS() {
  255. const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
  256. const oriGet = dataProperty.get;
  257. dataProperty.get = hookedGet;
  258. Object.defineProperty(MessageEvent.prototype, "data", dataProperty);
  259. function hookedGet() {
  260. const socket = this.currentTarget;
  261. if (!(socket instanceof WebSocket)) {
  262. return oriGet.call(this);
  263. }
  264. if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1 && socket.url.indexOf("api-test.milkywayidle.com/ws") <= -1) {
  265. return oriGet.call(this);
  266. }
  267. const message = oriGet.call(this);
  268. Object.defineProperty(this, "data", { value: message }); // Anti-loop
  269. try {
  270. return handleMessage(message);
  271. } catch (error) {
  272. console.log("Error in handleMessage:", error);
  273. return message;
  274. }
  275. }
  276. }
  277. hookWS();
  278. function handleMessage(message) {
  279. if (typeof message !== "string") {
  280. return message;
  281. }
  282. try {
  283. const parsedMessage = JSON.parse(message);
  284. if (parsedMessage && parsedMessage.type === "init_character_data") {
  285. selfData = parsedMessage;
  286. }
  287. } catch (error) {
  288. console.log("Error parsing message:", error);
  289. }
  290. return message;
  291. }
  292. function parsePrice(costText) {
  293. if (costText.endsWith('M')) {
  294. return parseFloat(costText.replace('M', '').replace(',', '')) * 1e6
  295. } else if (costText.endsWith('k')) {
  296. return parseFloat(costText.replace('k', '').replace(',', '')) * 1e3
  297. } else if (costText.endsWith('K')) {
  298. return parseFloat(costText.replace('K', '').replace(',', '')) * 1e3
  299. } else if (costText.endsWith('B')) {
  300. return parseFloat(costText.replace('B', '').replace(',', '')) * 1e9
  301. } else if (costText.endsWith('T')) {
  302. return parseFloat(costText.replace('T', '').replace(',', '')) * 1e12
  303. } else {
  304. return parseFloat(costText.replace(',', ''))
  305. }
  306. }
  307. function priceParse(priceNum) {
  308. const isNegative = priceNum < 0;
  309. const absValue = Math.abs(priceNum);
  310. let result;
  311. if (absValue >= 1e10) {
  312. result = parseFloat((absValue / 1e9).toFixed(3)) + 'B';
  313. } else if (absValue >= 1e7) {
  314. result = parseFloat((absValue / 1e6).toFixed(3)) + 'M';
  315. } else if (absValue >= 1e4) {
  316. result = parseFloat((absValue / 1e3).toFixed(3)) + 'K';
  317. } else {
  318. result = parseFloat(absValue.toFixed(3));
  319. }
  320. return isNegative ? '-' + result : '+' + result;
  321. }
  322. function getMWIToolsPrice(modal) {
  323. const enhancedPriceText = isZH ? '总成本' : 'Total cost';
  324. let costNodes = Array.from(modal.querySelectorAll('*')).filter(el => {
  325. if (!el.textContent || !el.textContent.includes(enhancedPriceText)) return false;
  326. return Array.from(el.childNodes).every(node => node.nodeType === Node.TEXT_NODE);
  327. });
  328. if (costNodes.length > 0) {
  329. const node = costNodes[0];
  330. const costText = node.textContent.replace(enhancedPriceText, '').trim();
  331. return parsePrice(costText);
  332. }
  333. const normalPriceText = isZH ? '日均价' : 'Daily average price';
  334. costNodes = Array.from(modal.querySelectorAll('*')).filter(el => {
  335. if (!el.textContent || !el.textContent.includes(normalPriceText)) return false;
  336. return Array.from(el.childNodes).every(node => node.nodeType === Node.TEXT_NODE);
  337. });
  338. if (costNodes.length > 0) {
  339. const node = costNodes[0];
  340. const costText = node.textContent.split('/')[0].split(' ')[1].trim();
  341. return parsePrice(costText);
  342. }
  343. return -1;
  344. }
  345. function getSelfStyleTrans() {
  346. const combatStyle = selfData?.combatUnit.combatDetails.combatStats.combatStyleHrids[0];
  347. return {
  348. '/combat_styles/stab': isZH ? '刺击' : 'Stab',
  349. '/combat_styles/slash': isZH ? '斩击' : 'Slash',
  350. '/combat_styles/smash': isZH ? '钝击' : 'Smash',
  351. '/combat_styles/ranged': isZH ? '远程' : 'Ranged',
  352. '/combat_styles/magic': isZH ? '魔法' : 'Magic',
  353. }[combatStyle] || combatStyle;
  354. }
  355. setInterval(() => {
  356. const modals = document.querySelectorAll('.MuiPopper-root');
  357. if (!modals) return;
  358. // compatibility with chat-enhance addon
  359. let equipmentDetail = null;
  360. let modal = null;
  361. for (const modal_ of modals) {
  362. equipmentDetail = modal_.querySelector('.ItemTooltipText_equipmentDetail__3sIHT');
  363. if (equipmentDetail) {
  364. modal = modal_;
  365. break
  366. };
  367. }
  368. if (!equipmentDetail) return;
  369. const equipmentName = modal.querySelector('.ItemTooltipText_name__2JAHA').textContent.trim();
  370. const equipmentData = parseEquipmentModal(equipmentDetail);
  371. const price = getMWIToolsPrice(modal);
  372. if (equipmentToDiff.data) {
  373. addDiffToModal(equipmentDetail, equipmentData, price);
  374. }
  375. document.onkeydown = (event) => {
  376. if (event.key === 'd') {
  377. // event.preventDefault();
  378. console.log(`Added to quick diff: ${equipmentName}`);
  379. equipmentToDiff = {
  380. data: equipmentData,
  381. name: equipmentName,
  382. price: price,
  383. };
  384. equipmentDetail.querySelector('.diff-tooltip').textContent = isZH ? '已添加作为对比' : 'Added to quick diff';
  385. } else if (event.key === 'r') {
  386. // event.preventDefault();
  387. console.log(`Removed from quick diff: ${equipmentName}`);
  388. equipmentToDiff = {};
  389. equipmentDetail.querySelector('.diff-tooltip').textContent = isZH ? '已移除对比' : 'Removed from quick diff';
  390. }
  391. };
  392. }, 500)
  393. // #region DPS Caculator
  394. const monsterStatusMap = {
  395. 50: {evasion: 80, resistence: 14},
  396. 80: {evasion: 130, resistence: 18},
  397. 100: {evasion: 170, resistence: 30},
  398. 120: {evasion: 250, resistence: 40},
  399. 140: {evasion: 300, resistence: 80},
  400. 200: {evasion: 400, resistence: 100},
  401. }
  402. const abilityDamgeMap = {
  403. 50: {ratio: 0.66, flat: 12},
  404. 80: {ratio: 0.69, flat: 13},
  405. 100: {ratio: 0.735, flat: 14.5},
  406. 120: {ratio: 0.78, flat: 16},
  407. 140: {ratio: 0.825, flat: 17.5},
  408. 200: {ratio: 0.87, flat: 19},
  409. }
  410. function chooseBelow(level, map) {
  411. const keys = Object.keys(map).map(Number).filter(k => k <= level);
  412. if (keys.length === 0) return null;
  413. const maxKey = Math.max(...keys);
  414. return map[maxKey];
  415. }
  416. function getDamageBase(combatStyle, skillLevel, atkAcc, atkAmp, dmgAmp, dmgPen, critAmp, critDmg) {
  417. const isMagic = combatStyle === "/combat_styles/magic";
  418. const abilityFlatDmg = isMagic ? chooseBelow(skillLevel, abilityDamgeMap)?.flat : 0;
  419. const abilityRatioDmg = isMagic ? chooseBelow(skillLevel, abilityDamgeMap)?.ratio + 1 : 1;
  420. const enemyEvasion = chooseBelow(skillLevel, monsterStatusMap)?.evasion || 80;
  421. const enemyResistence = chooseBelow(skillLevel, monsterStatusMap)?.resistence || 80;
  422. const accuracy = (10+skillLevel)*(1+atkAcc)
  423. const hitRate = Math.pow(accuracy, 1.4) / (Math.pow(accuracy, 1.4) + Math.pow(enemyEvasion, 1.4));
  424. const critRate = combatStyle == "/combat_styles/ranged" ? hitRate * 0.3 + critAmp : critAmp;
  425. const baseDmg = (10+skillLevel) * (1+atkAmp);
  426. const minDmg = (1+dmgAmp) * (1+abilityFlatDmg);
  427. const maxDmg = (1+dmgAmp) * (abilityRatioDmg*baseDmg + abilityFlatDmg);
  428. const estDmg = (minDmg + maxDmg) / 2 * (1-critRate) + maxDmg * critRate * (1+critDmg);
  429. const actDmg = estDmg * (100 / (100 + (enemyResistence / ( 1 + dmgPen))))
  430. return actDmg * hitRate;
  431. }
  432. function getSkillLevel(skillName, map) {
  433. for (const skill of map) {
  434. if (skill.skillHrid === skillName) {
  435. return skill.level;
  436. }
  437. }
  438. return -1
  439. }
  440. function getDPSCompare(diffMap) {
  441. const combatDetails = selfData?.combatUnit.combatDetails;
  442. const combatStyle = combatDetails.combatStats.combatStyleHrids[0];
  443. const damageType = combatDetails.combatStats.damageType;
  444. function getValueFromDiffMap(key) {
  445. const k = transZH(key);
  446. if (!diffMap || !diffMap[k]) return 0;
  447. const s = diffMap[k].replace(',', '').replace(' ', '').replace('+', '').replace('_', '')
  448. if (s.endsWith('%')) {
  449. return parseFloat(s.replace('%', '')) / 100;
  450. } else if (s.endsWith('s')) {
  451. return parseFloat(s.replace('s', '')) * 1000000000;
  452. } else {
  453. return parseFloat(s);
  454. }
  455. }
  456. let atkSkillLevel = getSkillLevel('/skills/attack', selfData.characterSkills);
  457. let dmgSkillLevel = getSkillLevel('/skills/power', selfData.characterSkills);
  458. let originalAmp = {
  459. atkAcc: 0,
  460. atkAmp: 0,
  461. dmgAmp: combatDetails.combatStats.physicalAmplify || 0,
  462. dmgPen: combatDetails.combatStats.armorPenetration || 0,
  463. autoDmg: combatDetails.combatStats.autoAttackDamage || 0,
  464. autoSpeed: combatDetails.combatStats.attackInterval || 3500000000,
  465. abilitySpeed: (100 + (combatDetails.combatStats.abilityHaste || 0))/100 * (1+(combatDetails.combatStats.castSpeed || 0)) / 2,
  466. critAmp: combatDetails.combatStats.criticalRate || 0,
  467. critDmg: combatDetails.combatStats.criticalDamage || 0,
  468. speedFactor: (1+(combatDetails.combatStats.attackSpeed || 0)) * (1 + atkSkillLevel / 2000) / combatDetails.combatStats.attackInterval,
  469. }
  470. let targetAmp = {
  471. atkAcc: 0,
  472. atkAmp: 0,
  473. dmgAmp: originalAmp.dmgAmp + getValueFromDiffMap('物理增幅'),
  474. dmgPen: originalAmp.dmgPen + getValueFromDiffMap('护甲穿透'),
  475. autoDmg: originalAmp.autoDmg + getValueFromDiffMap('自动攻击伤害'),
  476. autoSpeed: originalAmp.autoSpeed + getValueFromDiffMap('攻击间隔') * 1000000000,
  477. abilitySpeed: (100 + (combatDetails.combatStats.abilityHaste || 0) + getValueFromDiffMap('技能急速'))/100 * (1+(combatDetails.combatStats.castSpeed || 0)+getValueFromDiffMap('施法速度')) / 2,
  478. critAmp: originalAmp.critAmp + getValueFromDiffMap('暴击率'),
  479. critDmg: originalAmp.critDmg + getValueFromDiffMap('暴击伤害'),
  480. speedFactor: 0,
  481. otherAmp: 0,
  482. }
  483. originalAmp.otherAmp = 1 + originalAmp.autoDmg;
  484. targetAmp.otherAmp = 1 + targetAmp.autoDmg;
  485. targetAmp.speedFactor = (1+(combatDetails.combatStats.attackSpeed || 0)+getValueFromDiffMap('攻击速度')) * (1 + atkSkillLevel / 2000) / targetAmp.autoSpeed;
  486. switch (combatStyle) {
  487. case "/combat_styles/stab":
  488. originalAmp.atkAcc = combatDetails.stabAccuracyRating / (10 + atkSkillLevel) - 1;
  489. originalAmp.atkAmp = combatDetails.stabMaxDamage / (10 + dmgSkillLevel) - 1;
  490. targetAmp.atkAcc = originalAmp.atkAcc + getValueFromDiffMap('刺击精准度');
  491. targetAmp.atkAmp = originalAmp.atkAmp + getValueFromDiffMap('刺击伤害');
  492. break;
  493. case "/combat_styles/slash":
  494. originalAmp.atkAcc = combatDetails.slashAccuracyRating / (10 + atkSkillLevel) - 1;
  495. originalAmp.atkAmp = combatDetails.slashMaxDamage / (10 + dmgSkillLevel) - 1;
  496. targetAmp.atkAcc = originalAmp.atkAcc + getValueFromDiffMap('斩击精准度');
  497. targetAmp.atkAmp = originalAmp.atkAmp + getValueFromDiffMap('斩击伤害');
  498. break;
  499. case "/combat_styles/smash":
  500. originalAmp.atkAcc = combatDetails.smashAccuracyRating / (10 + atkSkillLevel) - 1;
  501. originalAmp.atkAmp = combatDetails.smashMaxDamage / (10 + dmgSkillLevel) - 1;
  502. targetAmp.atkAcc = originalAmp.atkAcc + getValueFromDiffMap('钝击精准度');
  503. targetAmp.atkAmp = originalAmp.atkAmp + getValueFromDiffMap('钝击伤害');
  504. break;
  505. case "/combat_styles/ranged":
  506. atkSkillLevel = getSkillLevel('/skills/ranged', selfData.characterSkills);
  507. dmgSkillLevel = atkSkillLevel;
  508. originalAmp.atkAcc = combatDetails.rangedAccuracyRating / (10 + atkSkillLevel) - 1;
  509. originalAmp.atkAmp = combatDetails.rangedMaxDamage / (10 + dmgSkillLevel) - 1;
  510. targetAmp.atkAcc = originalAmp.atkAcc + getValueFromDiffMap('远程精准度');
  511. targetAmp.atkAmp = originalAmp.atkAmp + getValueFromDiffMap('远程伤害');
  512. break;
  513. case "/combat_styles/magic":
  514. atkSkillLevel = getSkillLevel('/skills/magic', selfData.characterSkills);
  515. dmgSkillLevel = atkSkillLevel;
  516. originalAmp.atkAcc = combatDetails.magicAccuracyRating / (10 + atkSkillLevel) - 1;
  517. originalAmp.atkAmp = combatDetails.magicMaxDamage / (10 + dmgSkillLevel) - 1;
  518. originalAmp.speedFactor = originalAmp.abilitySpeed;
  519. targetAmp.atkAcc = originalAmp.atkAcc + getValueFromDiffMap('魔法精准度');
  520. targetAmp.atkAmp = originalAmp.atkAmp + getValueFromDiffMap('魔法伤害');
  521. targetAmp.speedFactor = targetAmp.abilitySpeed;
  522. switch (damageType) {
  523. case '/damage_types/fire':
  524. originalAmp.dmgAmp = combatDetails.combatStats.fireAmplify || 0;
  525. originalAmp.dmgPen = combatDetails.combatStats.firePenetration || 0;
  526. targetAmp.dmgAmp = originalAmp.dmgAmp + getValueFromDiffMap('火系增幅');
  527. targetAmp.dmgPen = originalAmp.dmgPen + getValueFromDiffMap('火系穿透');
  528. break;
  529. case '/damage_types/water':
  530. originalAmp.dmgAmp = combatDetails.combatStats.waterAmplify || 0;
  531. originalAmp.dmgPen = combatDetails.combatStats.waterPenetration || 0;
  532. targetAmp.dmgAmp = originalAmp.dmgAmp + getValueFromDiffMap('水系增幅');
  533. targetAmp.dmgPen = originalAmp.dmgPen + getValueFromDiffMap('水系穿透');
  534. break;
  535. case '/damage_types/nature':
  536. originalAmp.dmgAmp = combatDetails.combatStats.natureAmplify || 0;
  537. originalAmp.dmgPen = combatDetails.combatStats.naturePenetration || 0;
  538. targetAmp.dmgAmp = originalAmp.dmgAmp + getValueFromDiffMap('自然系增幅');
  539. targetAmp.dmgPen = originalAmp.dmgPen + getValueFromDiffMap('自然系穿透');
  540. break;
  541. default:
  542. console.warn(`Equipment Diff: Unknown damge style: ${damageType}`);
  543. return 0;
  544. }
  545. break;
  546. default:
  547. console.warn(`Equipment Diff: Unknown combat style: ${combatStyle}`);
  548. return 0;
  549. }
  550. const originalDamage = getDamageBase(combatStyle, atkSkillLevel, originalAmp.atkAcc, originalAmp.atkAmp, originalAmp.dmgAmp, originalAmp.dmgPen, originalAmp.critAmp, originalAmp.critDmg);
  551. const targetDamage = getDamageBase(combatStyle, atkSkillLevel, targetAmp.atkAcc, targetAmp.atkAmp, targetAmp.dmgAmp, targetAmp.dmgPen, targetAmp.critAmp, targetAmp.critDmg);
  552. const diffRatio = (targetDamage / originalDamage) * (targetAmp.otherAmp / originalAmp.otherAmp) * (targetAmp.speedFactor / originalAmp.speedFactor) - 1;
  553. return diffRatio
  554. }
  555. })();