[银河奶牛]钉钉

在公会页面显示更详细的数据

  1. // ==UserScript==
  2. // @name [Mwi]Guild Details Display
  3. // @name:zh-CN [银河奶牛]钉钉
  4. // @name:zh-TW [银河奶牛]公会数据显示
  5. // @namespace http://tampermonkey.net/
  6. // @version 1.05
  7. // @description Display more detailed data on the guild page.
  8. // @description:zh-cn 在公会页面显示更详细的数据
  9. // @description:zh-tw 在公会页面显示更详细的数据
  10. // @author Truth_Light
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com
  12. // @match https://www.milkywayidle.com/game?characterId=*
  13. // @grant none
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18. const userLanguage = navigator.language || navigator.userLanguage;
  19. const isZH = userLanguage.startsWith("zh");
  20. const updataDealy = 24*60*60*1000; //数据更新时限
  21. let rateXPDayMap = {};
  22. function formatPrice(value) {
  23. const isNegative = value < 0;
  24. value = Math.abs(value);
  25.  
  26. if (value >= 1000000) {
  27. return (isNegative ? '-' : '') + (value / 1000000).toFixed(1) + 'M';
  28. } else if (value >= 1000) {
  29. return (isNegative ? '-' : '') + (value / 1000).toFixed(1) + 'K';
  30. } else {
  31. return (isNegative ? '-' : '') + value.toFixed(0).toString();
  32. }
  33. }
  34.  
  35. function hookWS() {
  36. const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
  37. const oriGet = dataProperty.get;
  38.  
  39. dataProperty.get = hookedGet;
  40. Object.defineProperty(MessageEvent.prototype, "data", dataProperty);
  41.  
  42. function hookedGet() {
  43. const socket = this.currentTarget;
  44. if (!(socket instanceof WebSocket)) {
  45. return oriGet.call(this);
  46. }
  47. if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1 && socket.url.indexOf("api-test.milkywayidle.com/ws") <= -1) {
  48. return oriGet.call(this);
  49. }
  50.  
  51. const message = oriGet.call(this);
  52. Object.defineProperty(this, "data", { value: message }); // Anti-loop
  53.  
  54. return handleMessage(message);
  55. }
  56. }
  57.  
  58. function handleMessage(message) {
  59. try {
  60. let obj = JSON.parse(message);
  61. if (obj && obj.type === "guild_updated") {
  62. const Guild_ID = obj.guild.id;
  63. let storedData = JSON.parse(localStorage.getItem("Guild_Data")) || {};
  64.  
  65. // 判断是否已经存在旧数据
  66. if (storedData[Guild_ID] && storedData[Guild_ID].guild_updated && storedData[Guild_ID].guild_updated.old.updatedAt) {
  67. const oldUpdatedAt = new Date(storedData[Guild_ID].guild_updated.new.updatedAt);
  68. const newUpdatedAt = new Date(obj.guild.updatedAt);
  69.  
  70. // 计算时间差(单位:毫秒)
  71. const timeDifference = newUpdatedAt - oldUpdatedAt;
  72.  
  73. if (timeDifference >= updataDealy) {
  74. // 更新老数据为新数据
  75. storedData[Guild_ID].guild_updated.old = storedData[Guild_ID].guild_updated.new;
  76. // 更新新数据为当前数据
  77. storedData[Guild_ID].guild_updated.new = {
  78. experience: obj.guild.experience,
  79. level: obj.guild.level,
  80. updatedAt: obj.guild.updatedAt
  81. };
  82. } else {
  83. // 仅更新新数据
  84. storedData[Guild_ID].guild_updated.new = {
  85. experience: obj.guild.experience,
  86. level: obj.guild.level,
  87. updatedAt: obj.guild.updatedAt
  88. };
  89. }
  90. //计算Δ
  91. const Delta = {
  92. Delta_Xp: storedData[Guild_ID].guild_updated.new.experience - storedData[Guild_ID].guild_updated.old.experience,
  93. Delta_Level: storedData[Guild_ID].guild_updated.new.level - storedData[Guild_ID].guild_updated.old.level,
  94. Delta_Time: (newUpdatedAt - new Date(storedData[Guild_ID].guild_updated.old.updatedAt)) / 1000, // 转换为秒
  95. Rate_XP_Hours: (3600*(obj.guild.experience - storedData[Guild_ID].guild_updated.old.experience)/((newUpdatedAt - new Date(storedData[Guild_ID].guild_updated.old.updatedAt)) / 1000)).toFixed(2)
  96. };
  97. storedData[Guild_ID].guild_updated.Delta = Delta;
  98.  
  99. const Guild_TotalXp_div = document.querySelectorAll(".GuildPanel_value__Hm2I9")[1];
  100. if (Guild_TotalXp_div) {
  101. const xpText = isZH ? "经验值 / 小时" : "XP / Hour";
  102.  
  103. Guild_TotalXp_div.insertAdjacentHTML(
  104. "afterend",
  105. `<div>${formatPrice(Delta.Rate_XP_Hours)} ${xpText}</div>`
  106. );
  107. const Guild_NeedXp_div = document.querySelectorAll(".GuildPanel_value__Hm2I9")[2];
  108. if (Guild_NeedXp_div) {
  109. const Guild_NeedXp = document.querySelectorAll(".GuildPanel_value__Hm2I9")[2].textContent.replace(/,/g, '');
  110. const Time = TimeReset(Guild_NeedXp/Delta.Rate_XP_Hours);
  111. Guild_NeedXp_div.insertAdjacentHTML(
  112. "afterend", // 使用 "afterend" 在元素的后面插入内容
  113. `<div>${Time}</div>`
  114. );
  115. }
  116. }
  117. } else {
  118. // 如果没有旧数据,则直接添加新数据
  119. storedData[Guild_ID] = {
  120. guild_name: obj.guild.name,
  121. guild_updated: {
  122. old: {
  123. experience: obj.guild.experience,
  124. level: obj.guild.level,
  125. updatedAt: obj.guild.updatedAt
  126. },
  127. new: {},
  128. }
  129. };
  130. }
  131.  
  132. // 存储更新后的数据到 localStorage
  133. localStorage.setItem("Guild_Data", JSON.stringify(storedData));
  134. } else if (obj && obj.type === "guild_characters_updated") {
  135. let storedData = JSON.parse(localStorage.getItem("Guild_Data")) || {};
  136. for (const key in obj.guildSharableCharacterMap) {
  137. if (obj.guildSharableCharacterMap.hasOwnProperty(key)) {
  138. const Guild_ID = obj.guildCharacterMap[key].guildID;
  139. const name = obj.guildSharableCharacterMap[key].name;
  140. const newUpdatedAt = new Date();
  141. storedData[Guild_ID].guild_player = storedData[Guild_ID].guild_player || {};
  142. if (storedData[Guild_ID] && storedData[Guild_ID].guild_player && storedData[Guild_ID].guild_player[name] && storedData[Guild_ID].guild_player[name].old && storedData[Guild_ID].guild_player[name].old.updatedAt) {
  143. const oldUpdatedAt = new Date(storedData[Guild_ID].guild_player[name].old.updatedAt)
  144. const timeDifference = newUpdatedAt - oldUpdatedAt
  145. if (timeDifference >= updataDealy) {
  146. // 更新老数据为新数据
  147. storedData[Guild_ID].guild_player[name].old = storedData[Guild_ID].guild_player[name].new;
  148. // 更新新数据为当前数据
  149. storedData[Guild_ID].guild_player[name].new = {
  150. id: key,
  151. gameMode: obj.guildSharableCharacterMap[key].gameMode,
  152. guildExperience: obj.guildCharacterMap[key].guildExperience,
  153. updatedAt: newUpdatedAt,
  154. };
  155. } else {
  156. // 仅更新新数据
  157. storedData[Guild_ID].guild_player[name].new = {
  158. id: key,
  159. gameMode: obj.guildSharableCharacterMap[key].gameMode,
  160. guildExperience: obj.guildCharacterMap[key].guildExperience,
  161. updatedAt: newUpdatedAt,
  162. };
  163. }
  164. //计算Δ
  165. const Delta = {
  166. Delta_Time:(newUpdatedAt - new Date(storedData[Guild_ID].guild_player[name].old.updatedAt)) / 1000,
  167. Delta_Xp: storedData[Guild_ID].guild_player[name].new.guildExperience - storedData[Guild_ID].guild_player[name].old.guildExperience,
  168. Rate_XP_Day: (24*3600*(obj.guildCharacterMap[key].guildExperience - storedData[Guild_ID].guild_player[name].old.guildExperience)/((newUpdatedAt - new Date(storedData[Guild_ID].guild_player[name].old.updatedAt)) / 1000)).toFixed(2)
  169. };
  170. storedData[Guild_ID].guild_player[name].Delta = Delta;
  171. rateXPDayMap[name] = Delta.Rate_XP_Day;
  172. }else {
  173. storedData[Guild_ID].guild_player[name] = {
  174. old: {
  175. id: key,
  176. gameMode: obj.guildSharableCharacterMap[key].gameMode,
  177. guildExperience: obj.guildCharacterMap[key].guildExperience,
  178. updatedAt: newUpdatedAt,
  179. },
  180. new:{}
  181. };
  182. }
  183. }
  184.  
  185. }
  186. //console.log("测试数据",storedData);
  187. //console.log("guild_characters_updated", obj);
  188. updateExperienceDisplay(rateXPDayMap);
  189. localStorage.setItem("Guild_Data", JSON.stringify(storedData));
  190. }
  191.  
  192.  
  193.  
  194.  
  195. } catch (error) {
  196. console.error("Error processing message:", error);
  197. }
  198. return message;
  199. }
  200.  
  201.  
  202. function TimeReset(hours) {
  203. const totalMinutes = hours * 60;
  204. const days = Math.floor(totalMinutes / (24 * 60));
  205. const yudays = totalMinutes % (24 * 60);
  206. const hrs = Math.floor(yudays / 60);
  207. const minutes = Math.floor(yudays % 60);
  208. const dtext = isZH ? "天" : "d";
  209. const htext = isZH ? "时" : "h";
  210. const mtext = isZH ? "分" : "m";
  211. return `${days}${dtext} ${hrs}${htext} ${minutes}${mtext}`;
  212. }
  213.  
  214. function updateExperienceDisplay(rateXPDayMap) {
  215. const trElements = document.querySelectorAll(".GuildPanel_membersTable__1NwIX tbody tr");
  216. const idleuser_list = [];
  217. const dtext = isZH ? "天" : "d";
  218.  
  219. // 将 rateXPDayMap 转换为数组并排序
  220. const sortedMembers = Object.entries(rateXPDayMap)
  221. .map(([name, XPdata]) => ({ name, XPdata }))
  222. .sort((a, b) => b.XPdata - a.XPdata);
  223.  
  224. sortedMembers.forEach(({ name, XPdata }) => {
  225. trElements.forEach(tr => {
  226. const nameElement = tr.querySelector(".CharacterName_name__1amXp");
  227. const experienceElement = tr.querySelector("td:nth-child(3) > div");
  228. const activityElement = tr.querySelector('.GuildPanel_activity__9vshh');
  229.  
  230. if (nameElement && nameElement.textContent.trim() === name) {
  231. if (activityElement.childElementCount === 0) {
  232. idleuser_list.push(nameElement.textContent.trim());
  233. }
  234.  
  235. if (experienceElement) {
  236. const newDiv = document.createElement('div');
  237. newDiv.textContent = `${formatPrice(XPdata)}/${dtext}`;
  238.  
  239. // 计算颜色
  240. const rank = sortedMembers.findIndex(member => member.name === name);
  241. const hue = 120 - (rank * (120 / (sortedMembers.length - 1)));
  242. newDiv.style.color = `hsl(${hue}, 100%, 50%)`;
  243.  
  244. experienceElement.insertAdjacentElement('afterend', newDiv);
  245. }
  246. return;
  247. }
  248. });
  249. });
  250.  
  251. update_idleuser_tb(idleuser_list);
  252. }
  253.  
  254. function update_idleuser_tb(idleuser_list) {
  255. const targetElement = document.querySelector('.GuildPanel_noticeMessage__3Txji');
  256. if (!targetElement) {
  257. console.error('公会标语元素未找到!');
  258. return;
  259. }
  260. const clonedElement = targetElement.cloneNode(true);
  261.  
  262. const namesText = idleuser_list.join(', ');
  263. clonedElement.innerHTML = '';
  264. clonedElement.textContent = isZH ? `闲置的成员:${namesText}` : `Idle User : ${namesText}`;
  265. clonedElement.style.color = '#ffcc00';
  266.  
  267. // 设置复制元素的高度为原元素的25%
  268. const originalStyle = window.getComputedStyle(targetElement);
  269. const originalHeight = originalStyle.height;
  270. const originalMinHeight = originalStyle.minHeight;
  271. clonedElement.style.height = `25%`;
  272. clonedElement.style.minHeight = `25%`; // 也设置最小高度
  273. targetElement.parentElement.appendChild(clonedElement);
  274. }
  275.  
  276. hookWS();
  277.  
  278. })();