MZ - Player Ratings from MZLive

Displays player ratings on transfer and player pages

当前为 2025-04-06 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name MZ - Player Ratings from MZLive
  3. // @namespace douglaskampl
  4. // @version 1.7
  5. // @description Displays player ratings on transfer and player pages
  6. // @author Douglas
  7. // @match https://www.managerzone.com/?p=transfer*
  8. // @match https://www.managerzone.com/?p=players*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
  10. // @grant GM_addStyle
  11. // @run-at document-idle
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17.  
  18. const RATINGS = {
  19. "SPEED": { "K": 0.09, "D": 0.25, "A": 0.25, "M": 0.15, "W": 0.25, "F": 0.23 },
  20. "STAMINA": { "K": 0.09, "D": 0.16, "A": 0.18, "M": 0.15, "W": 0.20, "F": 0.15 },
  21. "PLAYINT": { "K": 0.09, "D": 0.07, "A": 0.05, "M": 0.10, "W": 0.06, "F": 0.05 },
  22. "PASSING": { "K": 0.02, "D": 0.02, "A": 0.05, "M": 0.15, "W": 0.04, "F": 0.04 },
  23. "SHOOTING": { "K": 0.00, "D": 0.00, "A": 0.00, "M": 0.00, "W": 0.05, "F": 0.28 },
  24. "HEADING": { "K": 0.00, "D": 0.00, "A": 0.02, "M": 0.00, "W": 0.00, "F": 0.03 },
  25. "GOALKEEPING": { "K": 0.55, "D": 0.00, "A": 0.00, "M": 0.00, "W": 0.00, "F": 0.00 },
  26. "BALLCONTROL": { "K": 0.09, "D": 0.08, "A": 0.10, "M": 0.12, "W": 0.15, "F": 0.15 },
  27. "TACKLING": { "K": 0.00, "D": 0.30, "A": 0.25, "M": 0.20, "W": 0.05, "F": 0.02 },
  28. "CROSSING": { "K": 0.02, "D": 0.07, "A": 0.05, "M": 0.08, "W": 0.15, "F": 0.00 },
  29. "SETPLAYS": { "K": 0.00, "D": 0.00, "A": 0.00, "M": 0.00, "W": 0.00, "F": 0.00 },
  30. "EXPERIENCE": { "K": 0.05, "D": 0.05, "A": 0.05, "M": 0.05, "W": 0.05, "F": 0.05 }
  31. };
  32. const SKILLS = [
  33. "SPEED",
  34. "STAMINA",
  35. "PLAYINT",
  36. "PASSING",
  37. "SHOOTING",
  38. "HEADING",
  39. "GOALKEEPING",
  40. "BALLCONTROL",
  41. "TACKLING",
  42. "CROSSING",
  43. "SETPLAYS",
  44. "EXPERIENCE",
  45. ];
  46.  
  47. function calculateRatings(skills) {
  48. const player = { K: 0, D: 0, A: 0, M: 0, W: 0, F: 0, B: 0, top: 0 };
  49.  
  50. SKILLS.forEach(skillName => {
  51. if (!skills[skillName] || !RATINGS[skillName]) return;
  52.  
  53. const value = parseInt(skills[skillName], 10);
  54. if (isNaN(value)) return;
  55.  
  56. if (skillName !== "EXPERIENCE") {
  57. player.B += value;
  58. }
  59.  
  60. Object.keys(player).forEach(pos => {
  61. if (pos !== 'B' && pos !== 'top') {
  62. const weight = RATINGS[skillName][pos];
  63. if (typeof weight === 'number') {
  64. player[pos] += value * weight;
  65. if (player[pos] > player.top) {
  66. player.top = player[pos];
  67. }
  68. }
  69. }
  70. });
  71. });
  72.  
  73. return {
  74. K: player.K.toFixed(2),
  75. D: player.D.toFixed(2),
  76. A: player.A.toFixed(2),
  77. M: player.M.toFixed(2),
  78. W: player.W.toFixed(2),
  79. F: player.F.toFixed(2),
  80. B: player.B,
  81. top: player.top.toFixed(2)
  82. };
  83. }
  84.  
  85. function extractSkillsFromTable(skillsTable) {
  86. const skills = {};
  87. if (!skillsTable) return skills;
  88.  
  89. const skillRows = skillsTable.querySelectorAll('tbody > tr');
  90.  
  91. skillRows.forEach((row, index) => {
  92. if (index >= SKILLS.length) return;
  93.  
  94. const valueElem = row.querySelector('td.skillval > span');
  95. if (valueElem) {
  96. const skillType = SKILLS[index];
  97. const value = valueElem.textContent.trim().replace(/[()]/g, '');
  98. if (skillType && value !== null && value !== '' && !isNaN(parseInt(value, 10))) {
  99. skills[skillType] = value;
  100. }
  101. }
  102. });
  103. return skills;
  104. }
  105.  
  106. function extractPlayerSkillsDirectly(playerElement) {
  107. const skillsTable = playerElement.querySelector('.player_skills');
  108. if (skillsTable) {
  109. return extractSkillsFromTable(skillsTable);
  110. }
  111. return {};
  112. }
  113.  
  114. function decodeHtmlEntities(text) {
  115. if (!text) return '';
  116. const textarea = document.createElement('textarea');
  117. textarea.innerHTML = text;
  118. return textarea.value;
  119. }
  120.  
  121. function fetchSkillsFromTransfer(playerId) {
  122. return new Promise((resolve, reject) => {
  123. const url = `https://www.managerzone.com/ajax.php?p=transfer&sub=transfer-search&sport=soccer&issearch=true&u=${playerId}&nationality=all_nationalities&deadline=0&category=&valuea=&valueb=&bida=&bidb=&agea=19&ageb=37&birth_season_low=56&birth_season_high=74&tot_low=0&tot_high=110&s0a=0&s0b=10&s1a=0&s1b=10&s2a=0&s2b=10&s3a=0&s3b=10&s4a=0&s4b=10&s5a=0&s5b=10&s6a=0&s6b=10&s7a=0&s7b=10&s8a=0&s8b=10&s9a=0&s9b=10&s10a=0&s10b=10&s11a=0&s11b=10&s12a=0&s12b=10&o=0`;
  124.  
  125. fetch(url, { credentials: 'include' })
  126. .then(response => {
  127. if (!response.ok) {
  128. throw new Error(`HTTP Error: ${response.status}`);
  129. }
  130. return response.json();
  131. })
  132. .then(data => {
  133. if (data && data.players) {
  134. try {
  135. const decodedHtml = decodeHtmlEntities(data.players);
  136. const parser = new DOMParser();
  137. const ajaxDoc = parser.parseFromString(decodedHtml, 'text/html');
  138. const skillsTable = ajaxDoc.querySelector('.player_skills');
  139. if (skillsTable) {
  140. const skills = extractSkillsFromTable(skillsTable);
  141. if (Object.keys(skills).length > 0) {
  142. resolve(skills);
  143. } else {
  144. reject("Could not extract skills from the AJAX response table.");
  145. }
  146. } else {
  147. reject("Skills table not found in AJAX response.");
  148. }
  149. } catch (e) {
  150. console.error("Error parsing AJAX response:", e);
  151. reject("Error parsing AJAX response: " + e.message);
  152. }
  153. } else {
  154. reject("No player data found in AJAX response.");
  155. }
  156. })
  157. .catch(error => {
  158. console.error("Error during fetch request:", error);
  159. reject("Error during fetch request: " + error.message);
  160. });
  161. });
  162. }
  163.  
  164. function createRatingDisplay(ratingsData) {
  165. const positions = [
  166. { code: 'K', name: 'Goalkeeper', value: ratingsData.K },
  167. { code: 'D', name: 'Defender', value: ratingsData.D },
  168. { code: 'A', name: 'Anchorman', value: ratingsData.A },
  169. { code: 'M', name: 'Midfielder', value: ratingsData.M },
  170. { code: 'W', name: 'Winger', value: ratingsData.W },
  171. { code: 'F', name: 'Forward', value: ratingsData.F }
  172. ];
  173.  
  174. const container = document.createElement('div');
  175. container.className = 'mz-rating-container';
  176.  
  177. const ratingsList = document.createElement('div');
  178. ratingsList.className = 'mz-rating-list';
  179.  
  180. positions.forEach(pos => {
  181. const row = document.createElement('div');
  182. row.className = 'mz-rating-row';
  183.  
  184. const isTop = pos.value === ratingsData.top;
  185.  
  186. const posName = document.createElement('span');
  187. posName.className = 'mz-pos-name' + (isTop ? ' mz-pos-top' : '');
  188. posName.textContent = pos.name + ':';
  189.  
  190. const posValue = document.createElement('span');
  191. posValue.className = 'mz-pos-value' + (isTop ? ' mz-pos-top' : '');
  192. posValue.textContent = pos.value;
  193.  
  194. row.appendChild(posName);
  195. row.appendChild(posValue);
  196. ratingsList.appendChild(row);
  197. });
  198.  
  199. container.appendChild(ratingsList);
  200.  
  201. const infoRow = document.createElement('div');
  202. infoRow.className = 'mz-rating-info-row';
  203. infoRow.innerHTML = `<span>Total Balls: <strong>${ratingsData.B}</strong></span> <span>Top: <strong>${ratingsData.top}</strong></span>`;
  204. container.appendChild(infoRow);
  205.  
  206. return container;
  207. }
  208.  
  209. function shouldAddButton(playerElement) {
  210. const skillsTable = playerElement.querySelector('.player_skills');
  211. if (skillsTable && skillsTable.querySelector('tbody > tr > td.skillval > span')) {
  212. return true;
  213. }
  214.  
  215. const isSinglePlayerPage = window.location.search.includes('pid=');
  216. const isOnTransferMarket = playerElement.querySelector('a[href*="p=transfer&sub=players&u="]');
  217.  
  218. if (isSinglePlayerPage && isOnTransferMarket) {
  219. return true;
  220. }
  221.  
  222. return false;
  223. }
  224.  
  225. function addRatingButton(playerElement) {
  226. const idElement = playerElement.querySelector('.player_id_span');
  227. if (!idElement) {
  228. return;
  229. }
  230.  
  231. const playerId = idElement.textContent.trim();
  232. if (!playerId) {
  233. return;
  234. }
  235.  
  236. if (!shouldAddButton(playerElement)) {
  237. return;
  238. }
  239.  
  240. if (idElement.parentNode.querySelector('.mz-rating-btn')) {
  241. return;
  242. }
  243.  
  244. const btn = document.createElement('button');
  245. btn.className = 'mz-rating-btn';
  246. btn.innerHTML = '<i class="fa-solid fa-calculator"></i>';
  247. btn.title = 'Show player ratings';
  248. btn.dataset.playerId = playerId;
  249.  
  250. let ratingContainer = null;
  251. let isVisible = false;
  252. let isLoading = false;
  253.  
  254. btn.addEventListener('click', async (e) => {
  255. e.preventDefault();
  256. e.stopPropagation();
  257.  
  258. if (isLoading) return;
  259.  
  260. if (isVisible && ratingContainer) {
  261. ratingContainer.classList.remove('mz-rating-visible');
  262. setTimeout(() => {
  263. if (ratingContainer && ratingContainer.parentNode) {
  264. ratingContainer.parentNode.removeChild(ratingContainer);
  265. }
  266. ratingContainer = null;
  267. }, 300);
  268. isVisible = false;
  269. btn.innerHTML = '<i class="fa-solid fa-calculator"></i>';
  270. btn.title = 'Show player ratings';
  271. return;
  272. }
  273.  
  274. isLoading = true;
  275. btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
  276. btn.title = 'Loading ratings...';
  277. let skills = {};
  278.  
  279. try {
  280. skills = extractPlayerSkillsDirectly(playerElement);
  281.  
  282. if (Object.keys(skills).length === 0) {
  283. const isSinglePlayerPage = window.location.search.includes('pid=');
  284. const isOnTransferMarket = playerElement.querySelector('a[href*="p=transfer&sub=players&u="]');
  285. if (isSinglePlayerPage && isOnTransferMarket) {
  286. skills = await fetchSkillsFromTransfer(playerId);
  287. }
  288. }
  289.  
  290. if (Object.keys(skills).length > 0) {
  291. const ratingsData = calculateRatings(skills);
  292. ratingContainer = createRatingDisplay(ratingsData);
  293.  
  294. const playerHeader = playerElement.querySelector('.subheader');
  295. const targetElement = playerHeader ? playerHeader.nextSibling : playerElement.firstChild;
  296. playerHeader.parentNode.insertBefore(ratingContainer, targetElement);
  297.  
  298. requestAnimationFrame(() => {
  299. requestAnimationFrame(() => {
  300. if (ratingContainer) ratingContainer.classList.add('mz-rating-visible');
  301. });
  302. });
  303.  
  304. isVisible = true;
  305. btn.innerHTML = '<i class="fa-solid fa-xmark"></i>';
  306. btn.title = 'Hide player ratings';
  307. } else {
  308. btn.innerHTML = '<i class="fa-solid fa-triangle-exclamation"></i>';
  309. btn.title = 'Could not retrieve skills';
  310. setTimeout(() => {
  311. if (!isVisible) {
  312. btn.innerHTML = '<i class="fa-solid fa-calculator"></i>';
  313. btn.title = 'Show player ratings';
  314. }
  315. }, 2000);
  316. }
  317.  
  318. } catch (error) {
  319. console.error(`Error getting ratings for player ${playerId}:`, error);
  320. btn.innerHTML = '<i class="fa-solid fa-triangle-exclamation"></i>';
  321. btn.title = `Error: ${error.message || error}`;
  322. setTimeout(() => {
  323. if (!isVisible) {
  324. btn.innerHTML = '<i class="fa-solid fa-calculator"></i>';
  325. btn.title = 'Show player ratings';
  326. }
  327. }, 3000);
  328. } finally {
  329. isLoading = false;
  330. if (!isVisible && !btn.innerHTML.includes('fa-triangle-exclamation')) {
  331. btn.innerHTML = '<i class="fa-solid fa-calculator"></i>';
  332. btn.title = 'Show player ratings';
  333. }
  334. }
  335. });
  336.  
  337. idElement.parentNode.insertBefore(btn, idElement.nextSibling);
  338. }
  339.  
  340. function processPlayerElements() {
  341. const playerContainers = document.querySelectorAll('div[id^="thePlayers_"]');
  342. playerContainers.forEach(container => {
  343. try {
  344. addRatingButton(container);
  345. } catch (e) {
  346. console.error("Error processing player container:", container, e);
  347. }
  348. });
  349. }
  350.  
  351. function setUpObserver() {
  352. const targetNode = document.getElementById('players_container')
  353. || document.querySelector('.mainContent')
  354. || document.body;
  355.  
  356. if (!targetNode) {
  357. console.error("MZ Ratings: Could not find a suitable node to observe for mutations.");
  358. return null;
  359. }
  360.  
  361. const observer = new MutationObserver((mutations) => {
  362. let needsProcessing = false;
  363. mutations.forEach(mutation => {
  364. if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  365. for (const node of mutation.addedNodes) {
  366. if (node.nodeType === Node.ELEMENT_NODE) {
  367. if ((node.id && node.id.startsWith('thePlayers_')) ||
  368. (node.querySelector && node.querySelector('div[id^="thePlayers_"]')))
  369. {
  370. needsProcessing = true;
  371. break;
  372. }
  373. }
  374. }
  375. }
  376. if(needsProcessing) return;
  377. });
  378.  
  379. if (needsProcessing) {
  380. setTimeout(processPlayerElements, 200);
  381. }
  382. });
  383.  
  384. observer.observe(targetNode, { childList: true, subtree: true });
  385. return observer;
  386. }
  387.  
  388. function addStyles() {
  389. GM_addStyle(
  390. `.mz-rating-btn {
  391. display: inline-flex;
  392. align-items: center;
  393. justify-content: center;
  394. margin-left: 8px;
  395. width: 20px;
  396. height: 20px;
  397. border: none;
  398. border-radius: 50%;
  399. background: #1a73e8;
  400. color: white;
  401. cursor: pointer;
  402. font-size: 12px;
  403. line-height: 1;
  404. vertical-align: middle;
  405. transition: all 0.2s ease;
  406. box-shadow: 0 1px 3px rgba(0,0,0,0.15);
  407. padding: 0;
  408. }
  409. .mz-rating-btn:hover {
  410. background: #0d5bbb;
  411. transform: translateY(-1px);
  412. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  413. }
  414. .mz-rating-btn > i {
  415. font-size: 12px;
  416. line-height: 1;
  417. vertical-align: baseline;
  418. }
  419. .mz-rating-container {
  420. margin: 10px 0 5px 5px;
  421. padding: 10px 12px;
  422. background: #f8f9fa;
  423. border: 1px solid #e0e0e0;
  424. border-radius: 6px;
  425. box-shadow: 0 1px 4px rgba(0,0,0,0.08);
  426. width: fit-content;
  427. opacity: 0;
  428. max-height: 0;
  429. overflow: hidden;
  430. transform: translateY(-10px);
  431. transition: all 0.3s ease-out;
  432. }
  433. .mz-rating-visible {
  434. opacity: 1;
  435. max-height: 500px;
  436. transform: translateY(0);
  437. margin-bottom: 10px;
  438. }
  439. .mz-rating-list {
  440. display: grid;
  441. grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  442. gap: 5px 10px;
  443. margin-bottom: 8px;
  444. }
  445. .mz-rating-row {
  446. display: flex;
  447. justify-content: space-between;
  448. align-items: center;
  449. padding: 2px 0px;
  450. font-size: 12px;
  451. }
  452. .mz-pos-name {
  453. color: #444;
  454. margin-right: 5px;
  455. }
  456. .mz-pos-value {
  457. font-weight: bold;
  458. color: #222;
  459. font-family: monospace;
  460. }
  461. .mz-pos-top {
  462. color: #1a73e8;
  463. font-weight: bold;
  464. }
  465. .mz-rating-info-row {
  466. margin-top: 8px;
  467. padding-top: 6px;
  468. border-top: 1px solid #e0e0e0;
  469. font-size: 11px;
  470. color: #555;
  471. display: flex;
  472. justify-content: space-between;
  473. }
  474. .mz-rating-info-row strong {
  475. color: #111;
  476. font-weight: 600;
  477. }`
  478. );
  479. }
  480.  
  481. function init() {
  482. addStyles();
  483. processPlayerElements();
  484. setUpObserver();
  485. }
  486.  
  487. if (document.readyState === 'complete' || document.readyState === 'interactive') {
  488. setTimeout(init, 300);
  489. } else {
  490. window.addEventListener('DOMContentLoaded', () => setTimeout(init, 300));
  491. }
  492. })();