MZ - Player Ratings from MZLive

Displays player ratings on transfer, player, and national team pages

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址