Advanced player analysis tool for FMP. Reads scout reports, calculates potential estimates, role suitability with error handling and export features.
当前为
// ==UserScript==
// @name FMP Player Analyzer v2.1
// @namespace http://tampermonkey.net/
// @version 2.1
// @description Advanced player analysis tool for FMP. Reads scout reports, calculates potential estimates, role suitability with error handling and export features.
// @author FMP Assistant
// @match https://footballmanagerproject.com/Team/Player*
// @icon https://www.google.com/s2/favicons?sz=64&domain=footballmanagerproject.com
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- 1. BUTTON AND STYLE ---
const analyzeBtn = document.createElement("button");
analyzeBtn.innerText = "📊 FULL ANALYSIS";
analyzeBtn.id = "fmpAnalyzeBtn";
analyzeBtn.style.position = "fixed";
analyzeBtn.style.top = "130px";
analyzeBtn.style.right = "20px";
analyzeBtn.style.zIndex = "9999";
analyzeBtn.style.padding = "10px 20px";
analyzeBtn.style.backgroundColor = "#2c3e50";
analyzeBtn.style.color = "#f1c40f";
analyzeBtn.style.border = "2px solid #f1c40f";
analyzeBtn.style.borderRadius = "5px";
analyzeBtn.style.cursor = "pointer";
analyzeBtn.style.fontWeight = "bold";
analyzeBtn.style.boxShadow = "0px 0px 10px rgba(0,0,0,0.5)";
document.body.appendChild(analyzeBtn);
// --- 2. ENHANCED ROLE DEFINITIONS ---
const ROLES = [
// Goalkeeper Roles
{ name_en: "Goalkeeper (GK)", keySkills: [0, 5, 9], category: "Goalkeeping" }, // Handling, Positioning, Heading
// Defensive Roles
{ name_en: "Central Defender (CD)", keySkills: [3, 4, 5, 9], category: "Defense" },
{ name_en: "Ball Playing Defender (BPD)", keySkills: [3, 4, 5, 6, 9], category: "Defense" },
{ name_en: "Full Back (FB)", keySkills: [1, 2, 3, 4, 7], category: "Defense" },
{ name_en: "Wing Back (WB)", keySkills: [1, 2, 3, 4, 7, 8], category: "Defense" },
{ name_en: "Defensive Midfielder (DM)", keySkills: [1, 4, 5, 6], category: "Defense" },
// Midfield Roles
{ name_en: "Deep Playmaker (DLP)", keySkills: [6, 8, 7, 1], category: "Midfield" },
{ name_en: "Central Midfielder (CM)", keySkills: [1, 6, 8, 5], category: "Midfield" },
{ name_en: "Box-to-Box Midfielder (BBM)", keySkills: [1, 2, 6, 8, 10], category: "Midfield" },
{ name_en: "Attacking Midfielder (AM)", keySkills: [6, 8, 10, 11], category: "Midfield" },
{ name_en: "Winger (W)", keySkills: [2, 7, 8, 6], category: "Midfield" },
{ name_en: "Inside Forward (IF)", keySkills: [2, 8, 10, 11], category: "Midfield" },
// Attacking Roles
{ name_en: "Advanced Forward (AF)", keySkills: [10, 2, 9, 11], category: "Attack" },
{ name_en: "Target Man (TM)", keySkills: [9, 10, 3, 1], category: "Attack" },
{ name_en: "Poacher (P)", keySkills: [10, 2, 11], category: "Attack" },
{ name_en: "Complete Forward (CF)", keySkills: [10, 9, 6, 8, 2], category: "Attack" }
];
// --- 3. IMPROVED SKILL DETECTION FUNCTIONS ---
function findSkillValues() {
console.log("🔍 Searching for skill values...");
// Method 1: Try multiple selectors for skill table
const selectors = [
'.skilltable span.num',
'.skilltable td span',
'.skilltable .num',
'table.skilltable span',
'.attributes-table span',
'.player-attributes span'
];
for (let selector of selectors) {
const elements = document.querySelectorAll(selector);
if (elements.length >= 12) {
console.log(`✅ Found ${elements.length} skills with selector: ${selector}`);
return Array.from(elements).map(el => {
const text = el.textContent.trim();
const value = parseFloat(text);
return isNaN(value) ? 0 : value;
});
}
}
// Method 2: Look for numeric values in tables
const tables = document.querySelectorAll('table');
for (let table of tables) {
const numbers = [];
const spans = table.querySelectorAll('span');
spans.forEach(span => {
const text = span.textContent.trim();
if (/^\d+$/.test(text)) {
const value = parseInt(text);
if (value >= 1 && value <= 20) {
numbers.push(value);
}
}
});
if (numbers.length >= 12) {
console.log(`✅ Found ${numbers.length} skills in table`);
return numbers;
}
}
// Method 3: Fallback - manual extraction from common FMP structure
const manualSkills = extractSkillsManually();
if (manualSkills.length >= 12) {
console.log(`✅ Found ${manualSkills.length} skills manually`);
return manualSkills;
}
console.log("❌ Could not find skill values");
return [];
}
function extractSkillsManually() {
const skills = [];
// Common FMP skill order
const skillNames = [
'Handling', 'Stamina', 'Pace', 'Marking', 'Tackling', 'Positioning',
'Passing', 'Crossing', 'Technique', 'Heading', 'Finishing', 'Longshots'
];
// Look for skill names and their values
skillNames.forEach(skillName => {
// Search for elements containing skill names
const elements = document.querySelectorAll('*');
for (let element of elements) {
if (element.textContent.includes(skillName)) {
// Look for nearby numeric values
let parent = element.parentElement;
for (let i = 0; i < 3; i++) {
if (parent) {
const numbers = parent.textContent.match(/\b\d{1,2}\b/g);
if (numbers) {
for (let num of numbers) {
const value = parseInt(num);
if (value >= 1 && value <= 20) {
skills.push(value);
return;
}
}
}
parent = parent.parentElement;
}
}
}
}
});
return skills;
}
function findPotentialValues() {
console.log("🔍 Searching for potential values...");
const potValues = {};
const potNames = ['Handling', 'Stamina', 'Pace', 'Marking', 'Tackling', 'Positioning', 'Passing', 'Crossing', 'Technique', 'Heading', 'Finishing', 'Longshots'];
// Method 1: Look for "Pot:" text
const allElements = document.querySelectorAll('*');
let foundCount = 0;
allElements.forEach(element => {
const text = element.textContent;
if (text && text.includes('Pot:')) {
const lines = text.split('\n');
lines.forEach(line => {
const potMatch = line.match(/Pot:\s*(\d+)/);
if (potMatch && foundCount < potNames.length) {
const value = parseInt(potMatch[1]);
if (!isNaN(value) && value >= 0 && value <= 20) {
potValues[potNames[foundCount]] = value;
foundCount++;
}
}
});
}
});
// Method 2: Look in skill development cells
if (foundCount < 8) {
const skillDevCells = document.querySelectorAll('td.skilldev, .skill-dev, .potential');
skillDevCells.forEach(cell => {
const text = cell.textContent;
potNames.forEach(name => {
if (text.includes(name) && text.includes('Pot:')) {
const potMatch = text.match(/Pot:\s*(\d+)/);
if (potMatch) {
const value = parseInt(potMatch[1]);
if (!isNaN(value)) {
potValues[name] = value;
}
}
}
});
});
}
console.log(`📊 Found ${Object.keys(potValues).length} potential values`);
return potValues;
}
// --- 4. MAIN ANALYSIS LOGIC ---
analyzeBtn.addEventListener("click", function() {
try {
performAnalysis();
} catch (error) {
console.error("FMP Analysis Error:", error);
showError("Analysis failed: " + error.message);
}
});
function performAnalysis() {
// --- A) FIND SKILL VALUES ---
const skillValues = findSkillValues();
if (skillValues.length < 12) {
showError(`Only found ${skillValues.length} skill values (need 12). The page structure might have changed.`);
return;
}
console.log("🎯 Skill values found:", skillValues);
// --- B) CURRENT STRENGTH CALCULATION ---
const getVal = (index) => skillValues[index] || 0;
const ca_phy = (getVal(1) + getVal(2)) / 2; // Stamina + Pace
const ca_def = (getVal(3) + getVal(4) + getVal(5)) / 3; // Marking + Tackling + Positioning
const ca_mid = (getVal(6) + getVal(8) + getVal(7)) / 3; // Passing + Technique + Crossing
const ca_att = (getVal(10) + getVal(9) + getVal(11)) / 3; // Finishing + Heading + Longshots
const totalCurrentScore = (ca_phy + ca_def + ca_mid + ca_att) / 4;
// --- C) ENHANCED ROLE SUITABILITY CALCULATION ---
let roleScores = [];
ROLES.forEach(role => {
let sum = 0;
let validSkills = 0;
role.keySkills.forEach(index => {
if (index < skillValues.length) {
sum += getVal(index);
validSkills++;
}
});
let average = validSkills > 0 ? sum / validSkills : 0;
roleScores.push({
name_en: role.name_en,
category: role.category,
score: average,
description: getRoleDescription(role.name_en)
});
});
// Sort by score and category
roleScores.sort((a, b) => b.score - a.score);
// --- D) PLAYER NAME EXTRACTION ---
const playerNameElement = document.querySelector('h1, .player-name, [class*="name"]');
const playerName = playerNameElement ? playerNameElement.innerText.trim() : "Unknown Player";
// --- E) SCOUT REPORT DETECTION ---
let reportData = {
summary: "---",
physical: "---",
defense: "---",
midfield: "---",
attack: "---",
blooming: "❓",
pro: "❓",
lead: "❓",
pers: "❓",
fit: "❓"
};
let isScouted = false;
let estimatedTotalPotential = 0;
// Scout report detection
const recDiv = document.querySelector('div.rec, .scout-report, .report');
if (recDiv) {
reportData.summary = recDiv.innerText.trim();
isScouted = true;
console.log("✅ Scout report found");
}
// Scouted data extraction
if (isScouted) {
const skillDevElements = document.querySelectorAll('td.skilldev, .skill-dev, .attribute-dev');
skillDevElements.forEach(element => {
let fullText = element.innerText;
const spans = element.querySelectorAll('span');
spans.forEach(span => {
if (span.title) fullText += "\n" + span.title;
});
const lines = fullText.split('\n');
lines.forEach(line => {
line = line.trim();
if (!line) return;
// Multi-language support
if (line.includes("Fizik:") || line.includes("Physical:")) reportData.physical = line.split(":")[1].trim();
if (line.includes("Savunma:") || line.includes("Defense:")) reportData.defense = line.split(":")[1].trim();
if (line.includes("Orta Saha:") || line.includes("Midfield:")) reportData.midfield = line.split(":")[1].trim();
if (line.includes("Hücum:") || line.includes("Attack:")) reportData.attack = line.split(":")[1].trim();
if (line.includes("Patlama:") || line.includes("Blooming:")) reportData.blooming = line.split(":")[1].trim();
if (line.includes("Profesyonellik: ") || line.includes("Professionalism: ")) reportData.pro = line.replace(/Profesyonellik: |Professionalism: /, "").trim();
if (line.includes("Liderlik: ") || line.includes("Leadership: ")) reportData.lead = line.replace(/Liderlik: |Leadership: /, "").trim();
if (line.includes("Kişilik: ") || line.includes("Personality: ")) reportData.pers = line.replace(/Kişilik: |Personality: /, "").trim();
if (line.includes("Fitness: ") || line.includes("Fitness: ")) reportData.fit = line.replace(/Fitness: |Fitness: /, "").trim();
});
});
// Convert scout rating (out of 25) to 20-point scale
const scoutRating = parseFloat(reportData.summary.split('/')[0]);
estimatedTotalPotential = !isNaN(scoutRating) ? scoutRating * (20/25) : totalCurrentScore;
}
// --- F) POTENTIAL ESTIMATION (Unscouted) ---
if (!isScouted) {
const potValues = findPotentialValues();
if (Object.keys(potValues).length >= 8) {
const getPotVal = (name) => potValues[name] || 0;
const pot_phy = (getPotVal('Stamina') + getPotVal('Pace')) / 2;
const pot_def = (getPotVal('Marking') + getPotVal('Tackling') + getPotVal('Positioning')) / 3;
const pot_mid = (getPotVal('Passing') + getPotVal('Technique') + getPotVal('Crossing')) / 3;
const pot_att = (getPotVal('Finishing') + getPotVal('Heading') + getPotVal('Longshots')) / 3;
// Use available data only
const availableCategories = [pot_phy, pot_def, pot_mid, pot_att].filter(val => val > 0);
estimatedTotalPotential = availableCategories.length > 0 ?
availableCategories.reduce((a, b) => a + b) / availableCategories.length : totalCurrentScore;
reportData.physical = pot_phy > 0 ? `${getEstimatedText(pot_phy)} (${pot_phy.toFixed(1)})` : `${getEstimatedText(ca_phy)} (${ca_phy.toFixed(1)})`;
reportData.defense = pot_def > 0 ? `${getEstimatedText(pot_def)} (${pot_def.toFixed(1)})` : `${getEstimatedText(ca_def)} (${ca_def.toFixed(1)})`;
reportData.midfield = pot_mid > 0 ? `${getEstimatedText(pot_mid)} (${pot_mid.toFixed(1)})` : `${getEstimatedText(ca_mid)} (${ca_mid.toFixed(1)})`;
reportData.attack = pot_att > 0 ? `${getEstimatedText(pot_att)} (${pot_att.toFixed(1)})` : `${getEstimatedText(ca_att)} (${ca_att.toFixed(1)})`;
} else {
// Fallback to current ability if no potential data
reportData.physical = `${getEstimatedText(ca_phy)} (${ca_phy.toFixed(1)})`;
reportData.defense = `${getEstimatedText(ca_def)} (${ca_def.toFixed(1)})`;
reportData.midfield = `${getEstimatedText(ca_mid)} (${ca_mid.toFixed(1)})`;
reportData.attack = `${getEstimatedText(ca_att)} (${ca_att.toFixed(1)})`;
estimatedTotalPotential = totalCurrentScore;
}
reportData.summary = "❓ Scouting Required";
}
// --- G) CREATE REPORT ---
createReportBox({
isScouted,
playerName,
totalCurrentScore,
estimatedTotalPotential,
reportData,
roleScores,
skillValues
});
}
// --- 5. HELPER FUNCTIONS ---
function showError(message) {
console.error("FMP Analyzer Error:", message);
const errorBox = document.createElement("div");
errorBox.style.position = "fixed";
errorBox.style.top = "50%";
errorBox.style.left = "50%";
errorBox.style.transform = "translate(-50%, -50%)";
errorBox.style.backgroundColor = "rgba(255,0,0,0.9)";
errorBox.style.color = "white";
errorBox.style.padding = "20px";
errorBox.style.borderRadius = "10px";
errorBox.style.zIndex = "10000";
errorBox.style.textAlign = "center";
errorBox.style.maxWidth = "80%";
errorBox.innerHTML = `
<h3>❌ FMP Analyzer Error</h3>
<p>${message}</p>
<p style="font-size: 12px; margin-top: 10px;">
Please make sure you're on a player page and try refreshing.
</p>
<button onclick="this.parentElement.remove()" style="margin-top: 10px; padding: 5px 15px; background: white; border: none; border-radius: 3px; cursor: pointer;">
Close
</button>
`;
document.body.appendChild(errorBox);
}
function getEstimatedText(value) {
if (value >= 17) return "VERY HIGH";
if (value >= 14) return "HIGH";
if (value >= 12) return "GOOD";
if (value >= 10) return "AVERAGE";
if (value >= 8) return "LOW";
return "VERY LOW";
}
function getEnhancedTextColor(value) {
if (value >= 17) return "#00ff00";
if (value >= 14) return "#a6f704";
if (value >= 12) return "#e1d919";
if (value >= 10) return "orange";
if (value >= 8) return "#ff4444";
return "#ff0000";
}
function getTextColor(text) {
const textStr = String(text).toUpperCase();
if (textStr.includes("VERY HIGH") || textStr.includes("EXCELLENT") || textStr.includes("OUTSTANDING")) return "#a6f704";
if (textStr.includes("HIGH") || textStr.includes("VERY GOOD")) return "#e1d919";
if (textStr.includes("GOOD") || textStr.includes("NORMAL")) return "yellow";
if (textStr.includes("AVERAGE") || textStr.includes("NORMAL")) return "orange";
if (textStr.includes("LOW") || textStr.includes("BAD") || textStr.includes("TERRIBLE")) return "#ff4444";
return "#ccc";
}
function getCategoryEmoji(category) {
const emojis = {
'Goalkeeping': '🧤',
'Defense': '🛡️',
'Midfield': '🎯',
'Attack': '⚽'
};
return emojis[category] || '🔹';
}
function getRoleDescription(roleName) {
const descriptions = {
"Central Defender (CD)": "Strong defensive skills, good in air",
"Ball Playing Defender (BPD)": "Good passing and technique for build-up",
"Full Back (FB)": "Stamina and crossing ability",
"Wing Back (WB)": "High stamina, pace, and crossing",
"Defensive Midfielder (DM)": "Defensive awareness and passing",
"Deep Playmaker (DLP)": "Excellent passing and technique",
"Central Midfielder (CM)": "Well-rounded midfield skills",
"Box-to-Box Midfielder (BBM)": "High stamina and all-around skills",
"Attacking Midfielder (AM)": "Creative and scoring ability",
"Winger (W)": "Pace, crossing and dribbling",
"Inside Forward (IF)": "Cutting inside and shooting",
"Advanced Forward (AF)": "Pace and finishing",
"Target Man (TM)": "Strength and heading",
"Poacher (P)": "Positioning and finishing",
"Complete Forward (CF)": "All-around attacking skills"
};
return descriptions[roleName] || "Role suitability based on key attributes";
}
// --- 6. REPORT BOX CREATION (same as before, but with improved error handling) ---
function createReportBox(data) {
let oldBox = document.getElementById("customReportBox");
if (oldBox) oldBox.remove();
const reportBox = document.createElement("div");
reportBox.id = "customReportBox";
reportBox.style.position = "fixed";
reportBox.style.top = "10%";
reportBox.style.left = "50%";
reportBox.style.transform = "translateX(-50%)";
reportBox.style.backgroundColor = "rgba(20, 30, 20, 0.98)";
reportBox.style.border = `2px solid ${data.isScouted ? '#a6f704' : '#00d2d3'}`;
reportBox.style.color = "white";
reportBox.style.zIndex = "10000";
reportBox.style.borderRadius = "10px";
reportBox.style.minWidth = "400px";
reportBox.style.maxWidth = "90vw";
reportBox.style.maxHeight = "85vh";
reportBox.style.overflow = "auto";
reportBox.style.fontFamily = "Arial, sans-serif";
reportBox.style.boxShadow = "0 0 25px black";
// ... (rest of the report box creation code remains the same as in v2.0)
// For brevity, I'm including the essential parts only
reportBox.innerHTML = `
<div id="reportHeader" style="cursor: move; background-color: #1a2500; padding: 12px; border-radius: 8px 8px 0 0; border-bottom: 1px solid #a6f704; display: flex; justify-content: space-between; align-items: center;">
<span style="color:${data.isScouted ? '#f1c40f' : '#00d2d3'}; font-weight:bold;">
${data.isScouted ? '📋 FMP FULL REPORT (Scouted)' : '⚠️ ESTIMATED POTENTIAL ANALYSIS (V2.1)'}
</span>
<span style="color:#ccc; font-size:11px; text-align: right;">
${data.playerName}<br>
Current: ${data.totalCurrentScore.toFixed(1)}
</span>
</div>
<div style="padding: 15px; font-size: 13px;">
<div style="background:rgba(255,255,255,0.05); padding:8px; border-radius:5px; margin-bottom:10px;">
<div style="display:flex; justify-content:space-between; border-bottom:1px solid #555; padding-bottom:5px; margin-bottom:5px;">
<span style="color:#a6f704; font-weight:bold;">
${data.isScouted ? 'SCOUT RATING (Total Potential):' : 'ESTIMATED POTENTIAL AVERAGE:'}
</span>
<span style="font-size:16px; font-weight:bold; color:white;">
${data.isScouted ? data.reportData.summary : data.estimatedTotalPotential.toFixed(1) + ' / 20'}
</span>
</div>
${!data.isScouted ? '<p style="font-size:11px; color:#00d2d3; margin: 5px 0 10px 0;">*This score is the average of Visible Potential Skill Scores (Pot:)</p>' : ''}
<table style="width:100%; color:#ddd;">
<tr><td>💪 Physical:</td> <td style="text-align:right; color:${getEnhancedTextColor(parseFloat(data.reportData.physical) || 0)}"><b>${data.reportData.physical}</b></td></tr>
<tr><td>🛡️ Defense:</td> <td style="text-align:right; color:${getEnhancedTextColor(parseFloat(data.reportData.defense) || 0)}"><b>${data.reportData.defense}</b></td></tr>
<tr><td>🎯 Midfield:</td> <td style="text-align:right; color:${getEnhancedTextColor(parseFloat(data.reportData.midfield) || 0)}"><b>${data.reportData.midfield}</b></td></tr>
<tr><td>⚽ Attack:</td> <td style="text-align:right; color:${getEnhancedTextColor(parseFloat(data.reportData.attack) || 0)}"><b>${data.reportData.attack}</b></td></tr>
</table>
</div>
<div style="text-align:center; margin-top:15px;">
<button id="closeReport" style="padding:6px 20px; cursor:pointer; background:#b32020; color:white; border:none; border-radius:4px;">CLOSE</button>
</div>
</div>
`;
document.body.appendChild(reportBox);
// Add drag functionality
dragElement(reportBox);
document.getElementById("closeReport").addEventListener("click", function(){
reportBox.remove();
});
}
function dragElement(elmnt) {
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
const header = document.getElementById("reportHeader");
if (header) {
header.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
const newTop = elmnt.offsetTop - pos2;
const newLeft = elmnt.offsetLeft - pos1;
if (newTop > 0 && newTop < window.innerHeight - 100) {
elmnt.style.top = newTop + "px";
}
if (newLeft > 0 && newLeft < window.innerWidth - 300) {
elmnt.style.left = newLeft + "px";
}
elmnt.style.transform = "none";
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
}
}
// --- 7. KEYBOARD SHORTCUT ---
document.addEventListener('keydown', function(e) {
if (e.altKey && e.key === 'A') {
e.preventDefault();
analyzeBtn.click();
}
});
console.log("FMP Player Analyzer v2.1 loaded successfully! Use Alt+A for quick analysis.");
})();