One modular ManagerZone soccer mega script: legacy mzcopilot plus non-overlapping tools from the selected scripts.
// ==UserScript== // @name mzcopilot Mega // @namespace mzcopilot // @version 5.3.1 // @license MIT // @description One modular ManagerZone soccer mega script: legacy mzcopilot plus non-overlapping tools from the selected scripts. // @author mzcopilot contributors and upstream authors // @match https://www.managerzone.com/* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/pako/2.0.4/pako.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/dexie.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/js/md5.min.js // @require https://unpkg.com/[email protected]/base64js.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/nprogress/0.2.0/nprogress.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/highcharts.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/html2canvas.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.3.2/spin.min.js // @grant GM_setValue // @grant GM_getValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_openInTab // @grant GM_registerMenuCommand // @grant unsafeWindow // @connect managerzone.com // @connect www.managerzone.com // @connect statsxente.com // @connect download06.managerzone.com // @connect mzcopilot.vercel.app // @run-at document-idle // ==/UserScript== /* mzcopilot mega body; generated from the canonical module sources */ window.__MZCOPILOT_FEATURE_OWNERSHIP__ = { "schemaVersion": 2, "features": [ { "id": "skill-colors-h1-l2", "owner": "legacy-player-advanced", "donors": ["legacy-player-advanced", "mzone-advanced"], "kind": "visual", "userscript": "implemented", "appTarget": "userscript-only", "app": "not-applicable" }, { "id": "player-radar-scout", "owner": "legacy-player-advanced", "donors": ["legacy-player-advanced", "mzone-advanced"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "match-player-prices-team-total", "owner": "legacy-player-advanced", "donors": ["legacy-player-advanced", "mazyar"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "clubhouse-dashboard-news", "owner": "legacy-player-advanced", "donors": ["legacy-player-advanced"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "match-2d-result", "owner": "legacy-player-advanced", "donors": ["legacy-player-advanced", "mzone-advanced"], "kind": "visual", "userscript": "implemented", "appTarget": "userscript-only", "app": "not-applicable" }, { "id": "local-tactic-xml", "owner": "legacy-player-advanced", "donors": ["legacy-player-advanced", "mzone-advanced"], "kind": "workflow", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "transfer-filters-deadlines-notes", "owner": "mazyar", "donors": ["mazyar"], "kind": "workflow", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "coach-salary-days-at-club", "owner": "mazyar", "donors": ["mazyar"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "league-live-table", "owner": "mzone-advanced", "donors": ["mzone-advanced", "mazyar", "ongoing-matches"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "fixture-difficulty", "owner": "mzone-advanced", "donors": ["mzone-advanced"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "factual-elo-context", "owner": "factual-insights", "donors": ["factual-insights", "stats-xente"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "implemented", "proof": ["apps/api/src/routes/userscript.ts", "apps/web/src/app/(dashboard)/insights/insights-client.tsx", "apps/web/src/components/factual-insights-panel.tsx"] }, { "id": "league-active-transfer-market", "owner": "factual-insights", "donors": ["factual-insights", "stats-xente", "mazyar"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "league-team-transfer-history", "owner": "factual-insights", "donors": ["factual-insights", "mazyar"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "implemented", "proof": ["apps/api/src/routes/userscript.ts", "apps/web/src/app/(dashboard)/league/league-client.tsx", "apps/web/src/components/factual-insights-panel.tsx"] }, { "id": "playoff-playout-predictor", "owner": "mzone-advanced", "donors": ["mzone-advanced"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "player-stat-averages", "owner": "mzone-advanced", "donors": ["mzone-advanced"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "shortlist-filter-history", "owner": "mzone-advanced", "donors": ["mzone-advanced"], "kind": "workflow", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "match-finder-statistics-summary", "owner": "mzone-advanced", "donors": ["mzone-advanced"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "retirement-residency-aging", "owner": "mzone-advanced", "donors": ["mzone-advanced"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "messenger-and-link-tools", "owner": "mzone-advanced", "donors": ["mzone-advanced"], "kind": "visual", "userscript": "implemented", "appTarget": "userscript-only", "app": "not-applicable" }, { "id": "tactic-library-crud-backup", "owner": "tactics-manager", "donors": ["tactics-manager", "axis"], "kind": "workflow", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "tactic-coordinate-touch-editor", "owner": "axis", "donors": ["axis"], "kind": "workflow", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "tactic-opponent-overlay", "owner": "axis", "donors": ["axis"], "kind": "visual", "userscript": "implemented", "appTarget": "userscript-only", "app": "not-applicable" }, { "id": "scheduled-opponent-tactics", "owner": "opponent-tactics", "donors": ["opponent-tactics"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "training-history-development", "owner": "training-history", "donors": ["training-history"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "weekly-match-participation", "owner": "weekly-matches", "donors": ["weekly-matches"], "kind": "factual", "userscript": "implemented", "appTarget": "shared", "app": "planned" }, { "id": "transfer-csv-export", "owner": "transfer-export", "donors": ["transfer-export", "mzone-advanced"], "kind": "workflow", "userscript": "implemented", "appTarget": "shared", "app": "planned" } ], "excluded": [ { "id": "auto-bid", "reason": "permanently excluded" }, { "id": "friendly-match-automation", "reason": "unattended game write" }, { "id": "ghost-tactic-bulk-write", "reason": "hidden or bulk game write" }, { "id": "hockey-expansion", "reason": "soccer-only scope" }, { "id": "federation-expansion", "reason": "no new federation work" } ] }; /* jQuery is supplied by a version-pinned @require. */ var __mzcJQuery = globalThis.jQuery; /* Pako 2 is shared by all modules on Greasy Fork. */ var __mzcPako1 = globalThis.pako; /* Dexie 4 is shared by all modules on Greasy Fork. */ var __mzcDexie3 = globalThis.Dexie; /* blueimp-md5 is supplied by a version-pinned @require. */ /* base64js is supplied by a version-pinned @require. */ var vpleModel={cacheByIDB:function(){this.db=new Dexie("MZCache");this.db.version(1).stores({cache:"key, html, ts"});this.getLocValue=function(e,t,s){this.db.cache.get(e,function(a){if(a==undefined){s(false);return}if(t==1){s(a.html);return}let i=a.ts;if(i!=-1){let e=new Date(i);let t=new Date;if(t.getUTCFullYear()==e.getUTCFullYear()&&t.getUTCMonth()==e.getUTCMonth()&&t.getUTCDate()==e.getUTCDate()){if(t.getUTCHours()>=1&&t.getUTCHours()<=22){}else if(t.getUTCHours()!=e.getUTCHours()){s(false);return}}else{s(false);return}s(a.html);return}else{s(false);return}})};this.setLocValue=function(e,t,a,i){let s=(new Date).getTime();if(a==1){let e=new Date;e.setDate(e.getDate()+30);s=e.getTime()}if(s>0){this.db.cache.put({key:e,html:t,ts:s}).then(function(e){i()})}else{i()}};this.clearCacheItem=function(e){this.db.cache.delete(e)};this.clearExpired=function(){let e=new Date(Date.now()-3600*1e3*24).getTime();this.db.cache.where("ts").below(e).delete()};this.clearAll=function(){this.db.cache.clear()}},cacheByGM:function(){this.getLocValue=function(s,e,l){s=md5(s);if(e==1){let e=GM_getValue(s,false);if(e){l(e);return}l(false);return}else{let i=GM_getValue("Dt_"+s,-1);if(i!=-1){let e=new Date(i);let t=new Date;if(t.getUTCFullYear()==e.getUTCFullYear()&&t.getUTCMonth()==e.getUTCMonth()&&t.getUTCDate()==e.getUTCDate()){if(t.getUTCHours()>=1&&t.getUTCHours()<=22){}else if(t.getUTCHours()!=e.getUTCHours()){l(false);return}}else{l(false);return}let a=GM_getValue(s,false);if(a){l(a);return}l(false);return}else{l(false);return}}};this.setLocValue=function(e,t,a,i){e=md5(e);let s=0;if(a==2){s=(new Date).getTime()}else if(a==1){let e=new Date;e.setDate(e.getDate()+30);s=e.getTime()}if(s>0){GM_setValue("Dt_"+e,s);GM_setValue(e,t)}i()};this.clearCacheItem=function(e){GM_deleteValue("Dt_"+e);GM_deleteValue(e)};this.clearExpired=function(e){let l=GM_listValues();let n=l.length;if(e){n=e}else if(l.length>100){n=100}for(let s=0;s<l.length;s++){let i;if(l[s].startsWith("Dt_")){i=GM_getValue(l[s],-1);let a=l[s].substring(3);if(i!=-1){let e=new Date(i);let t=new Date;if(t.getUTCFullYear()==e.getUTCFullYear()&&t.getUTCMonth()==e.getUTCMonth()&&t.getUTCDate()==e.getUTCDate()){continue}GM_deleteValue(l[s]);GM_deleteValue(a);n--;if(n<=0){break}}}}};this.clearAll=function(){this.clearExpired(100)};this.autoclearCache=function(){let e=GM_getValue("last_autoclear",0);let t=new Date(e);let a=new Date;if(a.getUTCFullYear()==t.getUTCFullYear()&&a.getUTCMonth()==t.getUTCMonth()&&a.getUTCDate()-t.getUTCDate()<3){return false}else{clearCache(100);GM_setValue("last_autoclear",a.getTime());return true}}},MatchEvent2:function(){this.data={};this.dataByPlayer={};this.playerFool={};this.longPass={};this.setData=function(e,r){this.match=e;let o=e.matchBuffer;let u={};let d={};let f={};let t={};let a={};r.out_of_play.resetIndex();let i;let h=0;let m={};let g={flag:false,data:{},begindown:false,state:0,owner:false};for(let n=0;n<o.length;n++){i=false;h=0;if(n-1>=0){if(o[n].ball.x==o[n-1].ball.x&&o[n].ball.y==o[n-1].ball.y&&o[n].ball.z==o[n-1].ball.z){i=false}else{i=true;h=o[n].ball.z-o[n-1].ball.z}}let l=o[n].players;for(let s=0;s<l.length;s++){if(l[s].status!=undefined){if(i&&r.out_of_play.notin(n)){if(t[l[s].id]==undefined){t[l[s].id]={};t[l[s].id].FoolStart=-1}else{if(t[l[s].id].x==l[s].position.x&&t[l[s].id].y==l[s].position.y&&t[l[s].id].z==l[s].position.z){if(t[l[s].id].FoolStart==-1){t[l[s].id].FoolStart=n-1;t[l[s].id].frame_count=0}t[l[s].id].frame_count++}else{if(t[l[s].id].FoolStart>0){if(a[l[s].id]==undefined){a[l[s].id]={};a[l[s].id].frame_count=0;a[l[s].id].data=new Array}let e={start:t[l[s].id].FoolStart,end:n-1,frame_count:t[l[s].id].frame_count};a[l[s].id].data.push(e);a[l[s].id].frame_count+=e.frame_count;t[l[s].id].FoolStart=-1;t[l[s].id].frame_count=0}}}t[l[s].id].x=l[s].position.x;t[l[s].id].y=l[s].position.y;t[l[s].id].z=l[s].position.z}if(g.flag&&g.owner.m_id==l[s].id){if(h<0){g.begindown=true;g.data.data.push(o[n].ball);g.data.end=n}else if(h==0&&o[n].ball.z<22||g.begindown&&(h>0||o[n].ball.z<22)){g.flag=false;let s=o[g.data.end].ball;let l=2e4,n=2e4;g.data.team=g.data.other=false;for(let e=0;e<o[g.data.end].players.length;e++){let i=o[g.data.end].players[e];if(i.position){let e;if(g.data.end>=r.m_ko2Frame){e=s.x-i.position.x}else{e=s.x-i.position.x}let t=s.y-i.position.y;let a=Math.sqrt(e*e+t*t);if(r.dit_player[i.id].m_side==g.owner.m_side){if(a<l){l=a;g.data.team={player:r.dit_player[i.id],gp:i,distance:a}}}else{if(a<n){n=a;g.data.other={player:r.dit_player[i.id],gp:i,distance:a}}}}}if(g.data.end>=r.m_ko2Frame){g.data.b=750-o[g.data.end].ball.x+","+(1e3-o[g.data.end].ball.y)+","+o[g.data.end].ball.z}else{g.data.b=o[g.data.end].ball.x+","+o[g.data.end].ball.y+","+o[g.data.end].ball.z}}else{g.data.data.push(o[n].ball);g.data.end=n}}if(l[s].status!=MatchStatus.BA_NORMAL){let e=r.dit_player[l[s].id];let t=e.m_side=="home"?true:false;if(g.flag==false){if(l[s].status==MatchStatus.BA_LEFT_FOOT_SHOT_FWD||l[s].status==MatchStatus.BA_RIGHT_FOOT_SHOT_FWD){if(g.data.end==n-1&&g.owner==e){g.flag=true;g.data.data.push(o[n].ball);g.data.end=n}else{if(n>=r.m_ko2Frame){g.data={h:"",a:750-o[n].ball.x+","+(1e3-o[n].ball.y)+","+o[n].ball.z,b:750-o[n].ball.x+","+(1e3-o[n].ball.y)+","+o[n].ball.z,start:n,end:n,data:[o[n].ball]}}else{g.data={h:"",a:o[n].ball.x+","+o[n].ball.y+","+o[n].ball.z,b:o[n].ball.x+","+o[n].ball.y+","+o[n].ball.z,start:n,end:n,data:[o[n].ball]}}g.state=0;g.begindown=false;g.flag=true;g.owner=e;if(m[l[s].id]==undefined){m[l[s].id]=new Array}m[l[s].id].push(g.data);m[l[s].id].owner=e}}}let a;if(d[l[s].status]==undefined){d[l[s].status]={};d[l[s].status][l[s].id]=a=new Array}else if(d[l[s].status][l[s].id]==undefined){d[l[s].status][l[s].id]=a=new Array}else{a=d[l[s].status][l[s].id]}let i=l[s].id+"_"+l[s].status+"_"+n;if(f[i]==undefined){a.push({m_frame:n,status:l[s].status,owner:e,isHome:t});f[i]=1}else{f[i]+=1}}if(u[l[s].id]==undefined){u[l[s].id]={}}if(u[l[s].id][n]==undefined){u[l[s].id][n]={}}u[l[s].id][n].data=l[s];u[l[s].id][n].ball_move=i;u[l[s].id][n].owner=o[n]}}}this.playerFool=a;let s={};let l={};this.data={};for(let a in d){if(s[a]==undefined){s[a]={}}if(this.data[a]==undefined){this.data[a]={}}for(let t in d[a]){if(l[t]==undefined){l[t]={};l[t].status=new Array;l[t].data=new Array}for(let e=0;e<d[a][t].length;e++){if(s[a][t]==undefined){s[a][t]={start:d[a][t][e].m_frame,last:d[a][t][e].m_frame,owner:d[a][t][e].owner,isHome:d[a][t][e].isHome}}else{if(s[a][t].last==d[a][t][e].m_frame-1){s[a][t].last=d[a][t][e].m_frame}else{if(this.data[a][t]==undefined){this.data[a][t]=new Array}this.data[a][t].push({m_frame_start:s[a][t].start,m_frame_end:s[a][t].last,owner:s[a][t].owner});l[t].data.push({m_frame_start:s[a][t].start,m_frame_end:s[a][t].last,status:parseInt(a)});s[a][t]={start:d[a][t][e].m_frame,last:d[a][t][e].m_frame,owner:d[a][t][e].owner,isHome:d[a][t][e].isHome}}}}}}for(let t in s){if(this.data[t]==undefined){this.data[t]={}}for(let e in s[t]){l[e].status.push(parseInt(t));if(this.data[t][e]==undefined){this.data[t][e]=new Array}l[e].owner=s[t][e].owner;l[e].isHome=s[t][e].isHome;this.data[t][e].push({m_frame_start:s[t][e].start,m_frame_end:s[t][e].last,owner:s[t][e].owner});l[e].data.push({m_frame_start:s[t][e].start,m_frame_end:s[t][e].last,status:parseInt(t)});l[e].data.sort(function(e,t){return e.m_frame_start-t.m_frame_start})}}this.dataByPlayer={};for(let s in l){this.dataByPlayer[s]={};this.dataByPlayer[s].isHome=l[s].isHome;this.dataByPlayer[s].owner=l[s].owner;this.dataByPlayer[s].status=l[s].status;this.dataByPlayer[s].data=new Array;let t=l[s].data;for(let e=0;e<t.length;e++){if(e+1<t.length){if(t[e].m_frame_end+1==t[e+1].m_frame_start){if(t[e].status==MatchStatus.BA_HEADER){if(t[e+1].status==MatchStatus.BA_LEFT_FOOT_SHOT_FWD){this.dataByPlayer[s].data.push({m_frame_start:t[e].m_frame_start,m_frame_end:t[e+1].m_frame_end,status:1001,old_arr:[t[e],t[e+1]]});e+=1;continue}else if(t[e+1].status==MatchStatus.BA_RIGHT_FOOT_SHOT_FWD){this.dataByPlayer[s].data.push({m_frame_start:t[e].m_frame_start,m_frame_end:t[e+1].m_frame_end,status:1002,old_arr:[t[e],t[e+1]]});e+=1;continue}else if(t[e+1].status==MatchStatus.BA_BALL_OWNER){this.dataByPlayer[s].data.push({m_frame_start:t[e].m_frame_start,m_frame_end:t[e+1].m_frame_end,status:1003,old_arr:[t[e],t[e+1]]});e+=1;continue}}if(t[e].status==MatchStatus.BA_TACKLE){if(t[e+1].status==MatchStatus.BA_LEFT_FOOT_SHOT_FWD||t[e+1].status==MatchStatus.BA_RIGHT_FOOT_SHOT_FWD||t[e+1].status==MatchStatus.BA_BALL_OWNER){this.dataByPlayer[s].data.push({m_frame_start:t[e].m_frame_start,m_frame_end:t[e].m_frame_end,status:1011,old_arr:[t[e]]});continue}}}}this.dataByPlayer[s].data.push(t[e])}this.dataByPlayer[s].FoolCount=0;for(let i=0;i<this.dataByPlayer[s].data.length;i++){let e=this.dataByPlayer[s].data[i];e.FoolCount=0;if(i+1<this.dataByPlayer[s].data.length&&this.dataByPlayer[s].data[i+1].m_frame_start==e.m_frame_end+1){continue}let t=e.m_frame_start;let a=u[s][t];t++;while(a!=undefined&&u[s][t]!=undefined){if(u[s][t].data.position.x==a.data.position.x&&u[s][t].data.position.y==a.data.position.y){e.FoolCount++;a=u[s][t];t++}else{break}}this.dataByPlayer[s].FoolCount+=e.FoolCount}}this.longPass=m};this.tolog=function(){let o=new Array;for(let r in this.longPass){this.longPass[r].mstat={};for(let e=0;e<this.longPass[r].length;e++){let n=this.longPass[r][e];if(n.team){n.h="[";n.team.events=this.getPlayerEventById(n.team.player.m_id,n.start,n.end);for(let t=0;t<n.team.events.length;t++){if(t>0){n.h+=","}n.h+=vgm.getMatchStatusName(n.team.events[t].status);if(n.team.events[t].status==1001||n.team.events[t].status==1002){let e=0;if(n.end<=vgm.m_htFrame){e=1}else if(n.end>=vgm.m_ko2Frame&&n.end<=vgm.m_ht2Frame){e=2}else if(n.end>=vgm.m_ko3Frame&&n.end<=vgm.m_ht3Frame){e=3}else if(n.end>=vgm.m_ko4Frame&&n.end<=vgm.m_ht4Frame){e=4}o.push({h:n.team.player.m_side,d:e,s:n.team.events[t].status,b:this.match.matchBuffer[n.end].ball,p:n.team.gp.position})}}n.h+="|";n.other.events=this.getPlayerEventById(n.other.player.m_id,n.start,n.end);for(let e=0;e<n.other.events.length;e++){if(e>0){n.h+=","}n.h+=vgm.getMatchStatusName(n.other.events[e].status)}n.h+="][";let e=this.match.matchBuffer[n.end].ball;let t=this.match.matchBuffer[n.start].ball;let a=0,i=0,s=0,l=0;if(n.end>=vgm.m_ko2Frame){a=n.team.gp.position.x-e.x;i=n.team.gp.position.y-e.y;s=n.other.gp.position.x-e.x;l=n.other.gp.position.y-e.y}else{a=e.x-n.team.gp.position.x;i=e.y-n.team.gp.position.y;s=e.x-n.other.gp.position.x;l=e.y-n.other.gp.position.y}n.h+=a+","+i+","+n.team.distance.toFixed(2)+";"+s+","+l+","+n.other.distance.toFixed(2)+"]"+(t.x-e.x)+","+(t.y-e.y)+","+(t.z-e.z);if(this.longPass[r].mstat[n.team.player.m_id]==undefined){this.longPass[r].mstat[n.team.player.m_id]={X:new vpleModel.MyMathArr,Y:new vpleModel.MyMathArr}}this.longPass[r].mstat[n.team.player.m_id].X.data.push(a);this.longPass[r].mstat[n.team.player.m_id].Y.data.push(i)}}for(let e in this.longPass[r].mstat){this.longPass[r].mstat[e].X.cal();this.longPass[r].mstat[e].Y.cal()}}console.log(this.longPass);let t="";for(let e=0;e<o.length;e++){t+=o[e].h+"\t"+o[e].d+"\t"+o[e].s+"\t"+o[e].b.x+"\t"+o[e].b.y+"\t"+o[e].b.z+"\t"+o[e].p.x+"\t"+o[e].p.y+"\t"+o[e].p.z+"\n"}console.log(t)};this.getPlayerEventById=function(a,i,s){let l=new Array;if(this.dataByPlayer[a]){for(let t=0;t<this.dataByPlayer[a].data.length;t++){let e=this.dataByPlayer[a].data[t];if(i<=e.m_frame_start&&e.m_frame_start<=s){l.push(e)}if(s<e.m_frame_start){return l}}}return l}},MyMathArr:function(){this.data=new Array;let e=function(e,t){return e+t};let a=function(e){return e*e};this.cal=function(){let t=this.data.reduce(e)/this.data.length;this.avg=t;this.deviations=this.data.map(function(e){return e-t});this.stddev=Math.sqrt(this.deviations.map(a).reduce(e)/(this.data.length-1));this.max=Math.max.apply(null,this.data);this.min=Math.min.apply(null,this.data);this.CV=this.stddev/this.avg}},tacCof:{GK:{ID:6,Name:"门将",SubCB:false,SubAtt:false,SubMF:false,SubExt:false,PRI:19,XMin:0,YMin:0,XMax:0,YMax:0,Speed:{90:15,100:20},Stamina:{90:20,100:25},Gameintelligence:{90:20,100:25},Passing:{100:1},Shooting:{},Heading:{},Goalkeeping:{90:40,100:50},Technique:{100:3},Tackling:{},Highpassing:{100:3},Situations:{100:12},Experience:{100:30},Form:{100:15}},CB:{ID:7,Name:"中卫",SubCB:true,SubAtt:false,SubMF:false,SubExt:false,PRI:16,XMin:281,YMin:658,XMax:715,YMax:1200,Speed:{100:18},Stamina:{100:18},Gameintelligence:{100:18},Passing:{100:8},Shooting:{},Heading:{100:15},Goalkeeping:{},Technique:{100:13},Tackling:{80:50,90:55,100:60},Highpassing:{100:10},Situations:{},Experience:{100:30},Form:{100:15}},RSB:{ID:9,Name:"右边卫",SubCB:false,SubAtt:false,SubMF:false,SubExt:false,PRI:13,XMin:716,YMin:658,XMax:1200,YMax:1200,Speed:{100:18},Stamina:{100:18},Gameintelligence:{100:15},Passing:{100:10},Shooting:{},Heading:{100:15},Goalkeeping:{},Technique:{100:15},Tackling:{100:40},Highpassing:{100:20},Situations:{},Experience:{100:30},Form:{100:15}},LSB:{ID:8,Name:"左边卫",SubCB:false,SubAtt:false,SubMF:false,SubExt:false,PRI:13,XMin:0,YMin:658,XMax:280,YMax:1200,Speed:{100:18},Stamina:{100:18},Gameintelligence:{100:15},Passing:{100:10},Shooting:{},Heading:{100:15},Goalkeeping:{},Technique:{100:15},Tackling:{100:40},Highpassing:{100:20},Situations:{},Experience:{100:30},Form:{100:15}},RM:{ID:12,Name:"右边中场",SubCB:false,SubAtt:false,SubMF:false,SubExt:false,PRI:12,XMin:800,YMin:502,XMax:1200,YMax:657,Speed:{100:18},Stamina:{100:18},Gameintelligence:{100:18},Passing:{100:15},Shooting:{100:1},Heading:{100:10},Goalkeeping:{},Technique:{100:15},Tackling:{100:40},Highpassing:{100:40},Situations:{},Experience:{100:30},Form:{100:15}},LM:{ID:12,Name:"左边中场",SubCB:false,SubAtt:false,SubMF:false,SubExt:false,PRI:12,XMin:0,YMin:502,XMax:201,YMax:657,Speed:{100:18},Stamina:{100:18},Gameintelligence:{100:18},Passing:{100:15},Shooting:{100:1},Heading:{100:10},Goalkeeping:{},Technique:{100:15},Tackling:{100:40},Highpassing:{100:40},Situations:{},Experience:{100:30},Form:{100:15}},CDM:{ID:10,Name:"后腰",SubCB:false,SubAtt:false,SubMF:false,SubExt:true,PRI:15,XMin:202,YMin:502,XMax:799,YMax:657,Speed:{100:18},Stamina:{100:20},Gameintelligence:{100:30},Passing:{100:15},Shooting:{100:3},Heading:{100:15},Goalkeeping:{},Technique:{100:20},Tackling:{100:40},Highpassing:{100:40},Situations:{},Experience:{100:30},Form:{100:15}},LMF:{ID:13,Name:"左边前卫",SubCB:false,SubAtt:false,SubMF:false,SubExt:false,PRI:11,XMin:0,YMin:347,XMax:201,YMax:501,Speed:{100:25},Stamina:{100:18},Gameintelligence:{100:15},Passing:{100:20},Shooting:{100:10},Heading:{100:2},Goalkeeping:{},Technique:{100:20},Tackling:{100:10},Highpassing:{100:30},Situations:{},Experience:{100:30},Form:{100:15}},RMF:{ID:14,Name:"右边前卫",SubCB:false,SubAtt:false,SubMF:false,SubExt:false,PRI:11,XMin:800,YMin:347,XMax:1200,YMax:501,Speed:{100:25},Stamina:{100:18},Gameintelligence:{100:15},Passing:{100:20},Shooting:{100:10},Heading:{100:2},Goalkeeping:{},Technique:{100:20},Tackling:{100:10},Highpassing:{100:30},Situations:{},Experience:{100:30},Form:{100:15}},CMF:{ID:15,Name:"中前卫",SubCB:false,SubAtt:false,SubMF:true,SubExt:false,PRI:14,XMin:202,YMin:347,XMax:799,YMax:501,Speed:{100:15},Stamina:{100:15},Gameintelligence:{100:20},Passing:{100:50},Shooting:{100:20},Heading:{100:3},Goalkeeping:{},Technique:{100:20},Tackling:{100:5},Highpassing:{100:30},Situations:{},Experience:{100:30},Form:{100:15}},LWF:{ID:17,Name:"左边锋",SubCB:false,SubAtt:false,SubMF:false,SubExt:false,PRI:17,XMin:0,YMin:0,XMax:201,YMax:346,Speed:{100:25},Stamina:{100:18},Gameintelligence:{100:15},Passing:{100:20},Shooting:{100:10},Heading:{},Goalkeeping:{},Technique:{90:20,100:30},Tackling:{100:5},Highpassing:{90:50,100:60},Situations:{},Experience:{100:30},Form:{100:15}},RWF:{ID:18,Name:"右边锋",SubCB:false,SubAtt:false,SubMF:false,SubExt:false,PRI:17,XMin:800,YMin:0,XMax:1200,YMax:346,Speed:{100:25},Stamina:{100:18},Gameintelligence:{100:15},Passing:{100:20},Shooting:{100:10},Heading:{},Goalkeeping:{},Technique:{90:20,100:30},Tackling:{100:5},Highpassing:{90:50,100:60},Situations:{},Experience:{100:30},Form:{100:15}},CF:{ID:16,Name:"前锋",SubCB:false,SubAtt:true,SubMF:false,SubExt:false,PRI:18,XMin:202,YMin:0,XMax:799,YMax:346,Speed:{100:30},Stamina:{100:20},Gameintelligence:{100:15},Passing:{100:5},Shooting:{80:40,90:45,100:50},Heading:{80:40,90:45,100:50},Goalkeeping:{},Technique:{100:15},Tackling:{},Highpassing:{},Situations:{},Experience:{100:30},Form:{100:15}}},TacPlayer:function(){this.Speed=0;this.Stamina=0;this.Gameintelligence=0;this.Passing=0;this.Shooting=0;this.Heading=0;this.Goalkeeping=0;this.Technique=0;this.Tackling=0;this.Highpassing=0;this.Situations=0;this.Form=0;this.Experience=0;this.InitByTds=function(e){if(e.length<13||vpleModel.GetEVal(e.eq(0))==null){return false}this.Speed=vpleModel.GetEVal(e.eq(0));this.Stamina=vpleModel.GetEVal(e.eq(1));this.Gameintelligence=vpleModel.GetEVal(e.eq(2));this.Passing=vpleModel.GetEVal(e.eq(3));this.Shooting=vpleModel.GetEVal(e.eq(4));this.Heading=vpleModel.GetEVal(e.eq(5));this.Goalkeeping=vpleModel.GetEVal(e.eq(6));this.Technique=vpleModel.GetEVal(e.eq(7));this.Tackling=vpleModel.GetEVal(e.eq(8));this.Highpassing=vpleModel.GetEVal(e.eq(9));this.Situations=vpleModel.GetEVal(e.eq(10));this.Experience=vpleModel.GetEVal(e.eq(11));this.Form=vpleModel.GetEVal(e.eq(12));return true};this.InitByImgs=function(e,t,a,i){if(e.length<13||e[0][t]==null){return false}this.Speed=e[0][t]*10;this.Stamina=e[1][t]*10;this.Gameintelligence=e[2][t]*10;this.Passing=e[3][t]*10;this.Shooting=e[4][t]*10;this.Heading=e[5][t]*10;this.Goalkeeping=e[6][t]*10;this.Technique=e[7][t]*10;this.Tackling=e[8][t]*10;this.Highpassing=e[9][t]*10;this.Situations=e[10][t]*10;this.Experience=a;this.Form=i;return true}},GetEVal:function(e){let t=e.parent().find(".skill_exact_bar").width();if(t==null){return null}let a=parseInt(e.html().match(vanGmMzModel.mzreg.td_skill_val)[1])*10;if(t==2){a+=2.5}else if(t==4){a+=5}else if(t==6){a+=7.5}return a}};var vple={cacheItem:new vpleModel.cacheByIDB,getLocValue:function(a,i,e,s){var l=this.cacheItem;l.getLocValue(a,e,function(t){if(t){let e;if(t.startsWith("H4sIAA")){if(s){e="9"+t}else{e=pako.ungzip(base64js.toByteArray(t),{to:"string"})}}else{if(s){e="9"+base64js.fromByteArray(pako.gzip(t))}else{e=t}}if(i(true,e)){l.clearCacheItem(a)}}else{i(false)}})},cbs:{},ajax:function(l,a,t,i){if(t==undefined){t=2}var n=this.cacheItem;var r=this.cbs;const s=function(){let e=false;let s=r[l];if(s==undefined){r[l]=s=[];e=true}else if(s.length==0){e=true}s.push({callback:a,Cjson:i,Cjson:i});if(e){$.ajax({type:"GET",url:l,dataType:"html",success:function(a){r[l]=[];let i=base64js.fromByteArray(pako.gzip(a));n.setLocValue(l,i,t,function(){let e=false;for(var t=0;t<s.length;t++){if(s[t].Cjson){e=s[t].callback("9"+i,false)||e}else{e=s[t].callback(a,false)||e}}if(e){n.clearCacheItem(l)}})},error:function(){r[l]=[]}})}};if(t>0){this.getLocValue(l,function(e,t){if(e){return a(t,true)}else{s()}},t,i)}else{s()}},autoclearCache:function(){let e=GM_getValue("last_autoclear",0);let t=new Date(e);let a=new Date;if(a.getUTCFullYear()==t.getUTCFullYear()&&a.getUTCMonth()==t.getUTCMonth()&&a.getUTCDate()-t.getUTCDate()<1){return false}else{this.cacheItem.clearExpired();GM_setValue("last_autoclear",a.getTime());return true}},report:function(){},jc:function(e,t){try{if(t==undefined){t=GM_getValue("jc",false)}if(t){e.eval(pako.ungzip(base64js.toByteArray(t),{to:"string"}));return true}}catch(e){console.error(e)}return false}}; /* ECharts is supplied by a version-pinned @require. */ "use strict"; (() => { // src/sync-bridge.ts var API_ORIGIN = "https://mzcopilot.vercel.app"; var TOKEN_KEY = "mzcopilot:sync:device-token"; var DEVICE_KEY = "mzcopilot:sync:device"; var LOCAL_PREFIX = "mzcopilot:sync:local:"; var SCHEMA_VERSION = 1; var DOMAIN_RE = /^[a-z][a-z0-9_-]{1,40}$/; var RAW_HTML_RE = /<(?:!doctype|html|body|script)\b/i; function containsRawHtml(value) { if (typeof value === "string") return RAW_HTML_RE.test(value); if (Array.isArray(value)) return value.some(containsRawHtml); if (!value || typeof value !== "object") return false; return Object.values(value).some(containsRawHtml); } function requestJsonBody(value) { return { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(value) }; } function createSyncBridge(deps) { return { async pair(code, name = "mzcopilot Mega") { const response = await deps.request( "/api/userscript/pair/exchange", requestJsonBody({ code: code.trim().toUpperCase(), name }) ); const body = await response.json(); if (!response.ok || !body.ok || !body.token || !body.deviceId) { throw new Error(body.error || "Pairing failed"); } deps.write(TOKEN_KEY, body.token); deps.write(DEVICE_KEY, { deviceId: body.deviceId, scopes: body.scopes ?? [], pairedAt: deps.now() }); return { deviceId: body.deviceId, scopes: body.scopes ?? [] }; }, async publish(domain, payload, syncedAt = deps.now()) { if (!DOMAIN_RE.test(domain)) throw new Error("invalid sync domain"); if (containsRawHtml(payload)) throw new Error("raw HTML cannot be synchronized"); deps.write(`${LOCAL_PREFIX}${domain}`, { schemaVersion: SCHEMA_VERSION, syncedAt, payload }); const token = deps.read(TOKEN_KEY, ""); if (!token) return { local: true, synced: false }; const batchId = `${deps.now()}-${deps.randomId()}`; const response = await deps.request("/api/ingest", { ...requestJsonBody({ schemaVersion: SCHEMA_VERSION, batchId, items: [{ domain, syncedAt, payload }] }), headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", "X-MZC-Schema-Version": String(SCHEMA_VERSION) } }); return { local: true, synced: response.ok }; }, status() { const token = deps.read(TOKEN_KEY, ""); const device = deps.read(DEVICE_KEY, {}); return { paired: Boolean(token), deviceId: device.deviceId }; } }; } var runtimeBridge = createSyncBridge({ read: (key, fallback) => typeof GM_getValue === "function" ? GM_getValue(key, fallback) : fallback, write: (key, value) => { if (typeof GM_setValue === "function") GM_setValue(key, value); }, request: (path, init) => fetch(`${API_ORIGIN}${path}`, init), now: () => Date.now(), randomId: () => crypto.randomUUID?.() ?? Math.random().toString(36).slice(2) }); function installSyncBridge(registerAction2) { window.__mzcSync = runtimeBridge; const status = runtimeBridge.status(); registerAction2( "pair-app", status.paired ? "App paired" : "Pair app", status.paired ? "This browser sends normalized factual snapshots to mzcopilot." : "Pair this browser with mzcopilot using a one-time code.", () => { const code = window.prompt("Enter the one-time pairing code from mzcopilot:"); if (!code) return; void runtimeBridge.pair(code, navigator.userAgent.includes("Mobile") ? "Mobile browser" : "Browser").then(() => { window.alert("mzcopilot paired. Reload ManagerZone to start synchronization."); }).catch((error) => { window.alert(error instanceof Error ? error.message : "Pairing failed"); }); } ); } // src/kernel.ts var STORAGE_PREFIX = "mzcopilot:mega:module:"; var records = []; var toolbarPanel = null; function getUiCopy() { const language = document.querySelector('meta[name="language"]')?.content.toLowerCase() ?? "en"; if (language.startsWith("es")) { return { modules: "M\xF3dulos", states: { started: "Activo", disabled: "Desactivado", skipped: "No se usa aqu\xED", failed: "Error" }, stateDescriptions: { started: "El m\xF3dulo est\xE1 activo en esta p\xE1gina.", disabled: "El m\xF3dulo est\xE1 desactivado.", skipped: "El m\xF3dulo est\xE1 habilitado, pero no corresponde a esta p\xE1gina.", failed: "El m\xF3dulo encontr\xF3 un error." }, help: "\xABNo se usa aqu\xED\xBB significa que el m\xF3dulo est\xE1 habilitado, pero corresponde a otra p\xE1gina. Recarga ManagerZone despu\xE9s de cambiar un m\xF3dulo." }; } return { modules: "Modules", states: { started: "Active", disabled: "Disabled", skipped: "Not on this page", failed: "Error" }, stateDescriptions: { started: "This module is active on the current page.", disabled: "This module is disabled.", skipped: "This module is enabled, but does not apply to the current page.", failed: "This module encountered an error." }, help: "\u201CNot on this page\u201D means the module is enabled but belongs to another page. Reload ManagerZone after changing a module." }; } function readEnabled(id) { const key = `${STORAGE_PREFIX}${id}`; const stored = typeof GM_getValue === "function" ? GM_getValue(key, true) : true; return stored !== false; } function writeEnabled(id, enabled) { if (typeof GM_setValue === "function") GM_setValue(`${STORAGE_PREFIX}${id}`, enabled); } function pageIsSoccer() { const sport = new URL(location.href).searchParams.get("sport"); return sport !== "hockey"; } function routeApplies(id) { const params = new URL(location.href).searchParams; const page = params.get("p"); const sub = params.get("sub"); if (id === "tactics-manager") { return page === "tactics" || page === "national_teams" && sub === "tactics"; } if (id === "axis") return page === "tactics"; if (id === "training-history") { return page === "players" || page === "transfer" && !location.href.includes("transfer_history"); } if (id === "transfer-export") return page === "transfer"; if (id === "opponent-tactics") return page === "match" && sub === "scheduled"; if (id === "weekly-matches") return page === "challenges"; if (id === "factual-insights") { return page === "league" || page === "federations" && sub === "clash" || page === "match" && ["played", "result", "scheduled"].includes(sub ?? ""); } return true; } function run(id, version, callback) { if (!pageIsSoccer() || !routeApplies(id)) { records.push({ id, version, state: "skipped" }); renderModuleControls(); return; } if (!readEnabled(id)) { records.push({ id, version, state: "disabled" }); renderModuleControls(); return; } try { callback(); records.push({ id, version, state: "started" }); } catch (error) { const message = error instanceof Error ? error.message : String(error); records.push({ id, version, state: "failed", error: message }); console.error(`[mzcopilot mega] ${id} failed`, error); } renderModuleControls(); } function injectToolbarStyles() { if (document.getElementById("mzc-mega-toolbar-styles")) return; const style = document.createElement("style"); style.id = "mzc-mega-toolbar-styles"; style.textContent = ` #mzc-mega-toolbar { position:fixed; z-index:2147483000; top:50%; right:0; transform:translateY(-50%); font:13px/1.35 system-ui,sans-serif; color:#17212b; } #mzc-mega-toolbar-toggle { width:42px; min-height:58px; padding:7px 5px; border:1px solid #82909c; border-right:0; border-radius:10px 0 0 10px; background:#14212b; color:#fff; cursor:pointer; font-weight:800; box-shadow:0 4px 18px #0007; } #mzc-mega-toolbar-panel { position:absolute; top:50%; right:48px; transform:translateY(-50%); width:min(440px,calc(100vw - 70px)); max-height:86vh; overflow:auto; padding:12px; border:1px solid #82909c; border-radius:10px; background:#f6f8fa; box-shadow:0 12px 36px #0008; } #mzc-mega-toolbar-panel[hidden] { display:none !important; } .mzc-mega-toolbar-header { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:8px; font-weight:800; font-size:15px; } .mzc-mega-toolbar-close { border:0; background:transparent; cursor:pointer; font-size:22px; line-height:1; color:#35424d; } .mzc-mega-toolbar-section { padding:9px 0; border-top:1px solid #d4d9de; } .mzc-mega-toolbar-section h3 { margin:0 0 7px; font:700 12px/1.2 system-ui,sans-serif; color:#52606b; text-transform:uppercase; letter-spacing:.04em; } .mzc-mega-toolbar-actions { display:flex; flex-wrap:wrap; align-items:center; gap:7px; } .mzc-mega-action { min-height:32px; padding:6px 9px; border:1px solid #9aa6b2; border-radius:6px; background:#fff; color:#17212b; cursor:pointer; font:600 12px/1.2 system-ui,sans-serif; } .mzc-mega-action:active { transform:scale(.97); } @media (hover:hover) and (pointer:fine) { .mzc-mega-action:hover { background:#e8f1f8; border-color:#4b7ca4; } } .mzc-mega-inline-surface { flex:1 0 100%; width:100%; min-width:0; box-sizing:border-box; } .mzc-mega-module-list { display:grid; gap:5px; width:100%; } .mzc-mega-module-row { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:start; gap:7px; padding:6px 7px; border:1px solid #d7dde2; border-radius:6px; background:#fff; } .mzc-mega-module-name { overflow-wrap:anywhere; font-size:12px; } .mzc-mega-module-state { padding:2px 5px; border-radius:999px; background:#e6ebef; color:#46545f; font-size:10px; text-transform:uppercase; } .mzc-mega-module-state[data-state="started"] { background:#dff3e5; color:#176b34; } .mzc-mega-module-state[data-state="failed"] { background:#fbe0e3; color:#9e1f2e; } .mzc-mega-help { margin:7px 0 0; color:#63717c; font-size:11px; } .mzc-mega-inline-settings { position:relative; padding:10px; border:1px solid #d1d8de; border-radius:7px; background:#fff; } .mzc-mega-inline-close { position:absolute; top:6px; right:6px; border:0; background:transparent; color:#52606b; cursor:pointer; font-weight:700; } #mzc-mega-toolbar-panel #mazyar-toolbar-overlay { position:static !important; inset:auto !important; display:flex !important; flex-direction:row !important; width:auto !important; height:auto !important; padding:6px !important; gap:8px !important; border-radius:6px; background:#14212b; } #mzc-mega-toolbar-panel #mazyar-toolbar-overlay > * { margin:0 !important; } #mzc-mega-toolbar-panel #mazyar-modal-overlay { position:static !important; inset:auto !important; width:100% !important; height:auto !important; padding:0 !important; background:transparent !important; } #mzc-mega-toolbar-panel #mazyar-modal { width:100% !important; min-width:0 !important; max-width:none !important; max-height:none !important; box-sizing:border-box; } #mzc-mega-toolbar-panel #mz-global-info-panel { position:static !important; inset:auto !important; width:100% !important; max-width:none !important; max-height:none !important; opacity:1 !important; transform:none !important; pointer-events:auto !important; box-shadow:none !important; border:0 !important; background:transparent !important; backdrop-filter:none !important; overflow:visible !important; display:block !important; font-family:inherit !important; } #mzc-mega-toolbar-panel .mz-gip-header { padding:10px 38px 10px 10px; border-radius:7px; } #mzc-mega-toolbar-panel .mz-gip-list { padding:6px 0; overflow:visible; } #mzc-mega-toolbar-panel .mz-gip-list a { padding:7px 9px; } #mzc-mega-toolbar-panel #mz-feature-settings-modal { position:static !important; inset:auto !important; width:100% !important; height:auto !important; background:transparent !important; z-index:auto !important; } #mzc-mega-toolbar-panel .mz-fs-content { width:100% !important; max-width:none !important; padding:10px !important; box-shadow:none !important; box-sizing:border-box; } #mzc-mega-toolbar-panel .mz-fs-body { grid-template-columns:1fr !important; } #mzc-mega-toolbar-panel #mz-quick-menu-container { position:static !important; inset:auto !important; transform:none !important; width:100% !important; color:#fff; border:0 !important; border-radius:7px !important; box-shadow:none !important; } #mzc-mega-toolbar-panel #mz-quick-menu-toggle, #mzc-mega-toolbar-panel .mz-quick-menu-close-btn { display:none !important; } #mzc-mega-toolbar-panel #mz-quick-menu-header { border-radius:7px 7px 0 0; } #mzc-mega-toolbar-panel #mz-quick-links-list { max-height:none; } @media (max-width:700px) { #mzc-mega-toolbar-panel { width:min(360px,calc(100vw - 58px)); right:45px; max-height:78vh; } #mzc-mega-toolbar-toggle { width:38px; } } `; (document.head ?? document.documentElement).appendChild(style); } function setToolbarOpen(open) { const panel = ensureToolbar(); panel.hidden = !open; document.getElementById("mzc-mega-toolbar-toggle")?.setAttribute("aria-expanded", String(open)); } function createToolbar() { injectToolbarStyles(); const root = document.createElement("aside"); root.id = "mzc-mega-toolbar"; const toggle = document.createElement("button"); toggle.id = "mzc-mega-toolbar-toggle"; toggle.type = "button"; toggle.textContent = "MZ+"; toggle.title = "mzcopilot tools and settings"; toggle.setAttribute("aria-expanded", "false"); const panel = document.createElement("div"); panel.id = "mzc-mega-toolbar-panel"; panel.hidden = true; const header = document.createElement("div"); header.className = "mzc-mega-toolbar-header"; header.append("mzcopilot Mega"); const close = document.createElement("button"); close.className = "mzc-mega-toolbar-close"; close.type = "button"; close.textContent = "\xD7"; close.title = "Close"; close.addEventListener("click", () => setToolbarOpen(false)); header.appendChild(close); panel.appendChild(header); toggle.addEventListener("click", () => setToolbarOpen(panel.hidden)); root.append(toggle, panel); (document.body ?? document.documentElement).appendChild(root); return panel; } function ensureToolbar() { toolbarPanel ?? (toolbarPanel = document.getElementById("mzc-mega-toolbar-panel") ?? createToolbar()); return toolbarPanel; } function ensureToolbarSection(id, label) { const panel = ensureToolbar(); const sectionId = `mzc-mega-section-${id}`; let actions = document.getElementById(sectionId)?.querySelector(".mzc-mega-toolbar-actions"); if (actions) return actions; const section = document.createElement("section"); section.id = sectionId; section.className = "mzc-mega-toolbar-section"; const heading = document.createElement("h3"); heading.textContent = label; actions = document.createElement("div"); actions.className = "mzc-mega-toolbar-actions"; section.append(heading, actions); panel.appendChild(section); return actions; } function registerAction(id, label, title, callback, domId) { const actions = ensureToolbarSection("actions", "Tools and settings"); const existing = document.querySelector(`[data-mzc-action="${id}"]`); const button = existing ?? document.createElement("button"); button.type = "button"; button.className = "mzc-mega-action"; button.dataset.mzcAction = id; button.textContent = label; button.title = title; if (domId) button.id = domId; if (!existing) { button.addEventListener("click", callback); actions.appendChild(button); } } function mountDonorToolbar(id, element, label) { if (!element || element.dataset.mzcMounted === "true") return; element.dataset.mzcMounted = "true"; ensureToolbarSection(id, label).appendChild(element); } function mountDonorContent(id, element, label) { if (!element || element.dataset.mzcMounted === "true") return; element.dataset.mzcMounted = "true"; element.classList.add("mzc-mega-inline-surface"); ensureToolbarSection(id, label).appendChild(element); } function showInlineSettings(id, label, html) { const actions = ensureToolbarSection(id, label); let surface = actions.querySelector(`[data-mzc-inline-settings="${id}"]`); if (!surface) { surface = document.createElement("div"); surface.className = "mzc-mega-inline-settings mzc-mega-inline-surface"; surface.dataset.mzcInlineSettings = id; const close = document.createElement("button"); close.type = "button"; close.className = "mzc-mega-inline-close"; close.textContent = "Hide"; close.addEventListener("click", () => { if (surface) surface.hidden = true; }); surface.appendChild(close); actions.appendChild(surface); } const body = surface.querySelector(".mzc-mega-inline-settings-body") ?? document.createElement("div"); body.className = "mzc-mega-inline-settings-body"; body.innerHTML = html; if (!body.parentElement) surface.appendChild(body); surface.hidden = false; setToolbarOpen(true); requestAnimationFrame(() => surface?.scrollIntoView({ block: "nearest" })); } function renderModuleControls() { const copy = getUiCopy(); const actions = ensureToolbarSection("modules", copy.modules); actions.replaceChildren(); const list = document.createElement("div"); list.className = "mzc-mega-module-list"; for (const record of records) { const row = document.createElement("label"); row.className = "mzc-mega-module-row"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = readEnabled(record.id); checkbox.addEventListener("change", () => writeEnabled(record.id, checkbox.checked)); const name = document.createElement("span"); name.className = "mzc-mega-module-name"; name.textContent = `${record.id} ${record.version ?? ""}${record.error ? ` \u2014 ${record.error}` : ""}`; const state = document.createElement("span"); state.className = "mzc-mega-module-state"; state.dataset.state = record.state; state.textContent = copy.states[record.state]; state.title = copy.stateDescriptions[record.state]; row.append(checkbox, name, state); list.appendChild(row); } const help = document.createElement("p"); help.className = "mzc-mega-help"; help.textContent = copy.help; actions.append(list, help); } function openDiagnostics() { renderModuleControls(); setToolbarOpen(true); document.getElementById("mzc-mega-section-modules")?.scrollIntoView({ block: "nearest" }); } window.__mzcMega = { run, records, openDiagnostics, registerAction, mountDonorToolbar, mountDonorContent, showInlineSettings }; renderModuleControls(); installSyncBridge(registerAction); if (typeof GM_registerMenuCommand === "function") { GM_registerMenuCommand("mzcopilot Mega: modules and diagnostics", openDiagnostics); } })(); /* mzcopilot:module:[email protected] */ "use strict"; (() => { // src/factual-market.ts function labels() { const language = document.querySelector('meta[name="language"]')?.getAttribute("content")?.toLowerCase() ?? document.documentElement.lang.toLowerCase(); if (language.startsWith("es")) { return { title: "Jugadores en venta de esta liga", help: "Se carga solo al hacer clic y limita las solicitudes para proteger ManagerZone.", load: "Cargar mercado activo", reload: "Actualizar mercado", loading: "Buscando jugadores en venta\u2026", empty: "No se encontraron jugadores en venta.", error: "No se pudo cargar el mercado de la liga.", headers: ["N\xB0", "Jugador", "Vendedor", "Comprador", "Edad", "Valor", "Oferta", "Cierre", "Ver"] }; } return { title: "Players on sale in this league", help: "Loaded only after your click with bounded requests to protect ManagerZone.", load: "Load active market", reload: "Refresh market", loading: "Finding players on sale\u2026", empty: "No players on sale were found.", error: "The league market could not be loaded.", headers: ["No.", "Player", "Seller", "Buyer", "Age", "Value", "Bid", "Deadline", "Show"] }; } async function fetchDocument(url) { const response = await fetch(url, { credentials: "include" }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return new DOMParser().parseFromString(await response.text(), "text/html"); } function idFromLink(link) { if (!link) return ""; try { return new URL(link.href, location.href).searchParams.get("tid") ?? ""; } catch { return ""; } } function compactNumber(container, selector) { const value = container.querySelector(selector)?.textContent?.trim() ?? ""; return value.replace(/\s+/g, " ") || "\u2014"; } function parseHeader(container, fallbackId) { const header = container.querySelector(".subheader.clearfix > div[title]"); const title = header?.getAttribute("title")?.trim() ?? ""; const match = title.match(/^(.+?)\s+\((\d+)\)\s+.+@\s+(.+)$/); const playerLink = container.querySelector("a.player_name, a[href*='pid=']"); return { id: match?.[2] ?? fallbackId, name: match?.[1]?.trim() || playerLink?.textContent?.trim() || fallbackId, deadline: match?.[3]?.trim() || header?.getAttribute("title")?.match(/\d{1,2}-\d{1,2}-\d{4}\s+\d{1,2}:\d{2}/)?.[0] || "\u2014" }; } function parseMarketPlayer(doc, playerId) { const container = doc.querySelector(".playerContainer"); if (!container) return null; const header = parseHeader(container, playerId); const bidButton = container.querySelector(".bid_button"); const bidTable = bidButton?.closest("table") ?? container.querySelector("table.bid-info"); const buyerLink = bidTable?.querySelector('a[href*="tid="]') ?? null; const sellerLink = container.querySelector( "a.seller-team, a[href*='p=team'][href*='tid=']" ); const flag = container.querySelector("img[src*='/flags/'], img.flag"); const flagMatch = flag?.src.match(/flags\/(?:\d+\/)?([^/.]+)\.png/i); const valueElement = container.querySelector( ".box_dark:not(.scout_report_row) span[title], .player-value" ); const age = compactNumber(container, ".dg_playerview_info tr:first-child strong, tr:first-child strong"); const bid = compactNumber(bidTable ?? container, "td strong, strong"); return { ...header, nationality: flagMatch?.[1] ?? flag?.title ?? "", sellerId: idFromLink(sellerLink), sellerName: sellerLink?.textContent?.trim() || "\u2014", buyerId: idFromLink(buyerLink), buyerName: buyerLink?.textContent?.trim() || "\u2014", age, value: valueElement?.getAttribute("title")?.match(/\d[\d\s.,]*/)?.[0]?.trim() || valueElement?.textContent?.trim() || "\u2014", bid, url: `/?p=transfer&sub=players&u=${encodeURIComponent(header.id)}` }; } async function boundedMap(items, worker) { const queue = items.map((item, index) => ({ item, index })); const results = new Array(items.length); const workers = Array.from({ length: Math.min(2, queue.length) }, async () => { while (queue.length) { const next = queue.shift(); if (!next) return; results[next.index] = await worker(next.item); } }); await Promise.all(workers); return results; } async function teamPlayerIds(target) { const doc = await fetchDocument(`/?p=players&tid=${encodeURIComponent(target.id)}`); const ids = Array.from(doc.querySelectorAll(".special_player")).map((icon) => icon.closest("a")).filter((link) => Boolean(link)).map((link) => new URL(link.href, location.href).searchParams.get("u")).filter((id) => Boolean(id)); return [...new Set(ids)]; } async function loadMarket(targets) { const teamIds = await boundedMap(targets, async (target) => { try { return await teamPlayerIds(target); } catch { return []; } }); const playerIds = [...new Set(teamIds.flat())]; const players = await boundedMap(playerIds, async (id) => { try { return parseMarketPlayer( await fetchDocument(`/?p=transfer&sub=players&u=${encodeURIComponent(id)}`), id ); } catch { return null; } }); return players.filter((player) => Boolean(player)); } function appendLinkedCell(row, text, href, className = "") { const cell = row.insertCell(); if (!href) { cell.textContent = text; return; } const link = document.createElement("a"); link.href = href; link.textContent = text; link.className = className; cell.appendChild(link); } function renderMarket(container, players) { const copy2 = labels(); if (!players.length) { container.textContent = copy2.empty; return; } const table = document.createElement("table"); table.className = "nice_table"; const head = table.createTHead().insertRow(); copy2.headers.forEach((label) => { const cell = document.createElement("th"); cell.textContent = label; head.appendChild(cell); }); const body = table.createTBody(); players.forEach((player, index) => { const row = body.insertRow(); row.insertCell().textContent = String(index + 1); appendLinkedCell(row, player.name, player.url); appendLinkedCell(row, player.sellerName, player.sellerId ? `/?p=team&tid=${player.sellerId}` : null); appendLinkedCell(row, player.buyerName, player.buyerId ? `/?p=team&tid=${player.buyerId}` : null); [player.age, player.value, player.bid, player.deadline].forEach((value) => { row.insertCell().textContent = value; }); appendLinkedCell(row, copy2.headers.at(-1) ?? "Show", player.url, "mzc-market-show"); }); container.replaceChildren(table); } function createPanel(table) { const copy2 = labels(); const section = document.createElement("section"); section.id = "mzc-league-market"; section.innerHTML = ` <div class="mzc-transfer-header"> <h3></h3><button type="button" id="mzc-league-market-load"></button><p></p> </div> <div id="mzc-league-market-results" aria-live="polite"></div>`; const title = section.querySelector("h3"); const button = section.querySelector("button"); const help = section.querySelector("p"); if (title) title.textContent = copy2.title; if (button) button.textContent = copy2.load; if (help) help.textContent = copy2.help; table.insertAdjacentElement("afterend", section); return section; } function mountLeagueMarket(table, targets) { if (document.getElementById("mzc-league-market")) return; const panel = createPanel(table); const button = panel.querySelector("#mzc-league-market-load"); const results = panel.querySelector("#mzc-league-market-results"); button?.addEventListener("click", async () => { if (!button || !results) return; button.disabled = true; results.textContent = labels().loading; try { renderMarket(results, await loadMarket(targets)); } catch { results.textContent = labels().error; } finally { button.textContent = labels().reload; button.disabled = false; } }); } // src/factual-insights.ts var XENTE_BASE = "https://statsxente.com/MZ1/Functions"; function publishSnapshot(domain, payload) { const sync = window.__mzcSync; if (!sync) return; void sync.publish(domain, payload).catch((error) => { console.warn(`[mzcopilot sync] ${domain}`, error); }); } function publishTeamRecords(context, targets, records) { publishSnapshot("elo-teams", { context, teams: Object.fromEntries(targets.map((target) => [ target.id, { name: target.name ?? target.link?.textContent?.trim() ?? target.id, elo: Number(records[target.id]?.elo ?? records[target.id]?.SENIOR) || null, lmValue: Number(records[target.id]?.valorUPSenior) || null } ])) }); } function uniqueIds(values) { const seen = /* @__PURE__ */ new Set(); return values.filter((value) => { if (!value || seen.has(value)) return false; seen.add(value); return true; }); } function buildIndexedUrl(base, params, indexedKey, ids) { const url = new URL(base); Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value)); ids.forEach((id, index) => url.searchParams.set(`${indexedKey}${index}`, id)); return url.toString(); } function formatEloDelta(value) { if (!Number.isFinite(value)) return null; const absolute = Math.abs(value).toFixed(2); if (value > 0) return `+${absolute}`; if (value < 0) return `\u2212${absolute}`; return absolute; } function isRecentMzDate(value, weeks, now = Date.now()) { const match = value.match(/(\d{1,2})-(\d{1,2})-(\d{4})/); if (!match) return false; const date = new Date(Number(match[3]), Number(match[2]) - 1, Number(match[1])); const age = now - date.getTime(); return age >= 0 && age <= weeks * 7 * 24 * 60 * 60 * 1e3; } function routeKind(href) { const url = new URL(href); if (url.searchParams.get("sport") === "hockey") return null; const page = url.searchParams.get("p"); const sub = url.searchParams.get("sub"); if (page === "league") return "league"; if (page === "federations" && sub === "clash") return "clash"; if (page === "match" && ["played", "result", "scheduled"].includes(sub ?? "")) { return "matches"; } return null; } function copy() { const language = document.querySelector('meta[name="language"]')?.getAttribute("content")?.toLowerCase() ?? document.documentElement.lang.toLowerCase(); if (language.startsWith("es")) { return { currentElo: "ELO actual", eloMovement: "Cambio ELO", transferHistory: "Historial de transferencias de la liga", transferHelp: "Carga bajo demanda para no hacer solicitudes ocultas por cada equipo.", loadTransfers: "Cargar \xFAltimas transferencias", reloadTransfers: "Actualizar transferencias", loading: "Cargando\u2026", noHistory: "No hay transferencias recientes.", loadError: "No se pudo cargar el historial.", weeks: "Semanas", team: "Equipo" }; } return { currentElo: "Current ELO", eloMovement: "ELO change", transferHistory: "League transfer history", transferHelp: "Loaded on demand so the script never makes hidden requests for every team.", loadTransfers: "Load recent transfers", reloadTransfers: "Refresh transfers", loading: "Loading\u2026", noHistory: "No recent transfers.", loadError: "Transfer history could not be loaded.", weeks: "Weeks", team: "Team" }; } function locale() { const language = document.querySelector('meta[name="language"]')?.getAttribute("content")?.toLowerCase() ?? "en"; return language.startsWith("es") ? "es-ES" : "en-US"; } function formatElo(value) { const number = Number(value); if (!Number.isFinite(number)) return null; const separator = locale().startsWith("es") ? "." : ","; return Math.round(number).toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator); } function extractId(href, key) { try { return new URL(href, location.href).searchParams.get(key); } catch { return null; } } function requestJson(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, headers: { Accept: "application/json" }, timeout: 12e3, onload: (response) => { if (response.status < 200 || response.status >= 300) { reject(new Error(`HTTP ${response.status}`)); return; } try { resolve(JSON.parse(response.responseText)); } catch (error) { reject(error); } }, onerror: () => reject(new Error("Network error")), ontimeout: () => reject(new Error("Request timeout")) }); }); } function injectStyles() { if (document.getElementById("mzc-factual-insights-style")) return; const style = document.createElement("style"); style.id = "mzc-factual-insights-style"; style.textContent = ` .mzc-elo-current,.mzc-elo-delta{display:inline-flex;align-items:center;gap:3px;margin-left:5px;padding:1px 5px;border-radius:999px;font:700 11px/1.5 Arial,sans-serif;white-space:nowrap} .mzc-elo-current{background:#e8f1fb;color:#174f7d} .mzc-elo-delta{background:#eef0f2;color:#38434d} .mzc-elo-delta[data-sign="positive"]{background:#ddf5e5;color:#146b34} .mzc-elo-delta[data-sign="negative"]{background:#fbe1e4;color:#9c2431} .mzc-elo-current-header,.mzc-elo-current-cell,.mzc-clash-elo,.mzc-clash-lm-value{text-align:center!important;white-space:nowrap} .mzc-match-elo-current{display:block;margin-top:2px;color:#222;font-weight:700;text-align:center} .mzc-match-elo-delta{display:block;margin:2px auto 0;padding:0;background:transparent;font-size:12px} #mzc-clash-elo-summary{display:flex;justify-content:center;gap:8px;flex-wrap:wrap;margin:8px auto;padding:8px} #mzc-clash-elo-summary>span{padding:5px 9px;border:1px solid #9bb4ca;border-radius:7px;background:#eef6fc;font-weight:700} #mzc-league-transfer-history,#mzc-league-market{margin-top:8px;border:1px solid #aab5be;border-radius:7px;overflow:hidden;background:#fff} #mzc-league-transfer-history .mzc-transfer-header,#mzc-league-market .mzc-transfer-header{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:8px;background:#e9eef2} #mzc-league-transfer-history h3,#mzc-league-market h3{flex:1 1 230px;margin:0;font-size:14px} #mzc-league-transfer-history p,#mzc-league-market p{flex:1 0 100%;margin:0;color:#596773;font-size:11px} #mzc-league-transfer-history button,#mzc-league-transfer-history select,#mzc-league-market button{min-height:29px;padding:4px 8px;border:1px solid #8e9ba6;border-radius:5px;background:#fff} #mzc-transfer-history-results,#mzc-league-market-results{padding:7px;overflow-x:auto} #mzc-transfer-history-results table{width:100%;border-collapse:collapse} #mzc-transfer-history-results td,#mzc-transfer-history-results th{padding:4px 6px} #mzc-league-market-results table{width:100%;border-collapse:collapse} #mzc-league-market-results td,#mzc-league-market-results th{padding:5px 7px;white-space:nowrap} #mzc-league-market-results tbody tr:nth-child(even){background:#ececec} .mzc-transfer-history-team td{padding-top:9px!important;border-top:2px solid #9da8b1;font-weight:800} `; (document.head ?? document.documentElement).appendChild(style); } function teamTargets(root) { const links = root.querySelectorAll( 'a[href*="p=team"][href*="tid="]' ); const seen = /* @__PURE__ */ new Set(); return Array.from(links).flatMap((link) => { const id = extractId(link.href, "tid"); if (!id || seen.has(id)) return []; seen.add(id); return [{ id, link, row: link.closest("tr") }]; }); } async function loadTeamRecords(targets) { const ids = uniqueIds(targets.map(({ id }) => id)); if (!ids.length) return {}; const url = buildIndexedUrl( `${XENTE_BASE}/tamper_teams.php`, { sport: "soccer" }, "idEquipo", ids ); return requestJson(url); } function appendInlineElo(target, record) { if (target.link.parentElement?.querySelector(".mzc-elo-current")) return; const value = formatElo(record?.elo ?? record?.SENIOR); if (!value) return; const badge = document.createElement("span"); badge.className = "mzc-elo-current"; badge.title = copy().currentElo; badge.textContent = `ELO ${value}`; target.link.insertAdjacentElement("afterend", badge); } function leagueTeamTable() { const tables = document.querySelectorAll("table.nice_table"); return Array.from(tables).find((table) => teamTargets(table).length >= 2) ?? null; } function appendLeagueEloColumn(table, targets, records) { if (table.querySelector(".mzc-elo-current-header")) return; const headerRow = table.querySelector("thead tr"); if (!headerRow) return; const header = document.createElement("th"); header.className = "mzc-elo-current-header"; header.textContent = "ELO"; header.title = copy().currentElo; headerRow.appendChild(header); targets.forEach((target) => { if (!target.row) return; const cell = target.row.insertCell(-1); cell.className = "mzc-elo-current-cell mzc-elo-current"; cell.textContent = formatElo(records[target.id]?.elo) ?? "\u2014"; }); } async function injectLeagueCurrentElo(table) { const targets = teamTargets(table); if (!targets.length) return; const records = await loadTeamRecords(targets); appendLeagueEloColumn(table, targets, records); publishTeamRecords("league", targets, records); } function clashTeamTable() { const tables = document.querySelectorAll("table.hitlist.challenges-list"); return Array.from(tables).find( (table) => Boolean(table.querySelector('a.team-name[href*="p=team"][href*="tid="]')) ) ?? null; } function clashTeamColumn(targets) { const first = targets.find((target) => target.row); const cell = first?.link.closest("td"); return first?.row && cell ? Array.from(first.row.cells).indexOf(cell) : -1; } function appendClashColumns(table, targets, records) { if (table.querySelector(".mzc-clash-elo-header")) return; const headerRow = table.querySelector("thead tr"); const teamColumn = clashTeamColumn(targets); if (!headerRow || teamColumn < 0) return; const eloHeader = document.createElement("th"); eloHeader.className = "mzc-clash-elo-header"; eloHeader.textContent = "ELO"; const valueHeader = document.createElement("th"); valueHeader.className = "mzc-clash-lm-value-header"; valueHeader.textContent = "LM Value"; headerRow.insertBefore(eloHeader, headerRow.cells[teamColumn] ?? null); headerRow.insertBefore(valueHeader, headerRow.cells[teamColumn + 1] ?? null); targets.forEach((target) => { if (!target.row) return; const eloCell = target.row.insertCell(teamColumn); eloCell.className = "mzc-clash-elo"; eloCell.textContent = formatElo(records[target.id]?.elo) ?? "\u2014"; const valueCell = target.row.insertCell(teamColumn + 1); valueCell.className = "mzc-clash-lm-value"; valueCell.textContent = formatElo(records[target.id]?.valorUPSenior) ?? "\u2014"; }); } async function injectClashTeamColumns() { const table = clashTeamTable(); if (!table) return; const targets = teamTargets(table); if (!targets.length) return; const records = await loadTeamRecords(targets); appendClashColumns(table, targets, records); publishTeamRecords("clash", targets, records); } async function injectInlineCurrentElo(root) { const targets = teamTargets(root); if (!targets.length) return; const records = await loadTeamRecords(targets); targets.forEach((target) => appendInlineElo(target, records[target.id])); } function compactText(value) { return (value ?? "").replace(/\s+/g, " ").trim().slice(0, 160); } function matchTargets(root) { const result = /* @__PURE__ */ new Map(); root.querySelectorAll(".score-cell-wrapper").forEach((mount) => { const link = mount.querySelector( 'a.score-shown[href*="mid="], a[href*="p=match"][href*="mid="]' ); const id = link ? extractId(link.href, "mid") : null; if (id && /\d+\s*-\s*\d+/.test(mount.textContent ?? "")) { result.set(id, { mount, compact: true, label: compactText(mount.closest("dd.odd, tr")?.textContent) }); } }); root.querySelectorAll('a[href*="p=match"][href*="mid="]').forEach((link) => { const id = extractId(link.href, "mid"); const context = `${link.textContent ?? ""} ${link.parentElement?.textContent ?? ""}`; if (id && /\d+\s*-\s*\d+/.test(context) && !result.has(id)) { result.set(id, { mount: link, compact: false, label: compactText(link.closest("dd.odd, tr")?.textContent ?? link.parentElement?.textContent) }); } }); return result; } function appendEloDelta(target, mid, rawValue) { if (document.querySelector(`[data-mzc-elo-mid="${CSS.escape(mid)}"]`)) return; const value = formatEloDelta(Number(rawValue)); if (!value) return; const badge = document.createElement("span"); badge.className = "mzc-elo-delta"; badge.dataset.mzcEloMid = mid; badge.dataset.sign = value.startsWith("+") ? "positive" : value.startsWith("\u2212") ? "negative" : "neutral"; badge.title = copy().eloMovement; if (target.compact) { badge.classList.add("mzc-match-elo-delta"); badge.textContent = `${value} ${value.startsWith("+") ? "\u25B2" : value.startsWith("\u2212") ? "\u25BC" : "\u25CF"}`; target.mount.appendChild(badge); return; } badge.textContent = `ELO ${value}`; target.mount.insertAdjacentElement("afterend", badge); } async function injectEloDeltas(root, clash) { const targets = matchTargets(root); if (!targets.size) return; const ids = [...targets.keys()]; const url = clash ? buildIndexedUrl( `${XENTE_BASE}/tamper_clash_matches_elo.php`, { sport: "soccer", currency: String(GM_getValue("currency", "USD")) }, "idPartido", ids ) : `${XENTE_BASE}/tamper_elo_change_matches.php?sport=soccer&mids=${encodeURIComponent(ids.join(","))}`; const changes = await requestJson(url); targets.forEach((target, mid) => appendEloDelta(target, mid, changes[mid])); publishSnapshot("elo-matches", { context: clash ? "clash" : "matches", matches: Object.fromEntries(ids.map((id) => [ id, { delta: Number(changes[id]) || 0, label: targets.get(id)?.label ?? "" } ])) }); } function ownTeamId() { return document.querySelector("#tid1")?.value || null; } function matchTeamTargets(root) { return Array.from(root.querySelectorAll( ".home-team-column, .away-team-column" )).flatMap((container) => { const link = container.querySelector( 'a.clippable[href*="p=team"], a[href*="p=team"]' ); if (!link) return []; const id = extractId(link.href, "tid") ?? ownTeamId(); return id ? [{ id, name: link.textContent?.trim() || id, container }] : []; }); } function appendMatchElo(target, record) { if (target.container.querySelector(".mzc-match-elo-current")) return; const value = formatElo(record?.elo ?? record?.SENIOR); if (!value) return; const label = document.createElement("span"); label.className = "mzc-match-elo-current"; label.title = copy().currentElo; label.textContent = value; target.container.appendChild(label); } async function injectMatchCurrentElo(root) { const targets = matchTeamTargets(root); if (!targets.length) { await injectInlineCurrentElo(root); return; } const records = await loadTeamRecords(targets); targets.forEach((target) => appendMatchElo(target, records[target.id])); publishTeamRecords("matches", targets, records); } function federationIds() { const badges = document.querySelectorAll(".fed_badge"); return uniqueIds(Array.from(badges).map((badge) => badge.src.match(/[?&]fid=(\d+)/)?.[1])); } function renderClashSummary(ids, records) { if (document.getElementById("mzc-clash-elo-summary")) return; const target = document.querySelector(".top-pane__deadline"); if (!target) return; const names = document.querySelectorAll(".name-score.text-ellipsis"); const panel = document.createElement("div"); panel.id = "mzc-clash-elo-summary"; ids.forEach((id, index) => { const item = document.createElement("span"); item.dataset.mzcFederationElo = id; const name = names[index]?.textContent?.trim(); const rank = records[id]?.table_index; const value = formatElo(records[id]?.elo) ?? "\u2014"; item.title = [name, rank ? `#${rank}` : ""].filter(Boolean).join(" \xB7 "); item.textContent = `ELO ${value}`; panel.appendChild(item); }); target.appendChild(panel); } async function injectClashSummary() { const ids = federationIds(); if (ids.length < 2) return; const [home, away] = ids; if (!home || !away) return; const url = `${XENTE_BASE}/tamper_federations_clash_data.php?sport=soccer¤cy=${encodeURIComponent(String(GM_getValue("currency", "USD")))}&home=${encodeURIComponent(home)}&away=${encodeURIComponent(away)}`; const records = await requestJson(url); renderClashSummary(ids, records); publishSnapshot("elo-clash", { federations: Object.fromEntries(ids.map((id) => [ id, { name: document.querySelectorAll(".name-score")[ids.indexOf(id)]?.textContent?.trim() ?? "", elo: Number(records[id]?.elo) || null, rank: Number(records[id]?.table_index) || null } ])) }); } function createTransferPanel(table) { const section = document.createElement("section"); section.id = "mzc-league-transfer-history"; const labels2 = copy(); section.innerHTML = ` <div class="mzc-transfer-header"> <h3>${labels2.transferHistory}</h3> <label>${labels2.weeks} <select id="mzc-transfer-history-weeks"> <option value="1">1</option><option value="2">2</option> <option value="3">3</option><option value="4" selected>4</option> </select></label> <button type="button" id="mzc-transfer-history-load">${labels2.loadTransfers}</button> <p>${labels2.transferHelp}</p> </div> <div id="mzc-transfer-history-results" aria-live="polite"></div>`; table.insertAdjacentElement("afterend", section); return section; } async function fetchTransferHistory(target, weeks) { const url = `${location.origin}/?p=transfer_history&tid=${encodeURIComponent(target.id)}`; const response = await fetch(url, { credentials: "include" }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const doc = new DOMParser().parseFromString(await response.text(), "text/html"); const rows = Array.from(doc.querySelectorAll( "#transfer-history tbody tr.bought, #transfer-history tbody tr.sold" )).filter((row) => { const value = row.querySelector("td")?.textContent?.trim() ?? ""; return isRecentMzDate(value, weeks); }); return { id: target.id, name: target.link.textContent?.trim() || target.id, url, rows }; } async function boundedTransferHistories(targets, weeks) { const queue = [...targets]; const results = []; const workers = Array.from({ length: Math.min(2, queue.length) }, async () => { while (queue.length) { const target = queue.shift(); if (!target) break; try { results.push(await fetchTransferHistory(target, weeks)); } catch { results.push({ id: target.id, name: target.link.textContent?.trim() || target.id, url: "", rows: [] }); } } }); await Promise.all(workers); return results.sort((a, b) => targets.findIndex((item) => item.id === a.id) - targets.findIndex((item) => item.id === b.id)); } function normalizedTransferHistories(histories, weeks) { return { weeks, leagueUrl: location.href, teams: histories.map((history) => ({ teamId: history.id, teamName: history.name, teamUrl: history.url || `https://www.managerzone.com/?p=team&tid=${history.id}`, transfers: history.rows.map((row) => { const cells = Array.from(row.cells).map((cell) => cell.textContent?.trim() ?? ""); return { direction: row.classList.contains("bought") ? "bought" : "sold", date: cells[0] ?? "", player: cells[1] ?? "", counterparty: cells[2] ?? "", amount: cells[3] ?? "" }; }) })) }; } function renderTransferHistories(container, histories) { const labels2 = copy(); const table = document.createElement("table"); table.className = "nice_table"; table.innerHTML = "<thead><tr><th>Date</th><th>Player</th><th>From / To</th><th>Fee / Value</th></tr></thead>"; const body = document.createElement("tbody"); histories.forEach((history) => { const heading = document.createElement("tr"); heading.className = "mzc-transfer-history-team"; heading.innerHTML = `<td colspan="4"><a href="${history.url || `/?p=team&tid=${history.id}`}"></a></td>`; const link = heading.querySelector("a"); if (link) link.textContent = history.name; body.appendChild(heading); history.rows.forEach((row) => body.appendChild(row.cloneNode(true))); }); table.appendChild(body); container.replaceChildren( histories.some((history) => history.rows.length) ? table : document.createTextNode(labels2.noHistory) ); } function mountTransferHistory(table) { if (document.getElementById("mzc-league-transfer-history")) return; const panel = createTransferPanel(table); const button = panel.querySelector("#mzc-transfer-history-load"); const weeks = panel.querySelector("#mzc-transfer-history-weeks"); const results = panel.querySelector("#mzc-transfer-history-results"); button?.addEventListener("click", async () => { if (!button || !weeks || !results) return; button.disabled = true; results.textContent = copy().loading; const histories = await boundedTransferHistories(teamTargets(table), Number(weeks.value)); renderTransferHistories(results, histories); publishSnapshot("league-transfers", normalizedTransferHistories(histories, Number(weeks.value))); button.textContent = copy().reloadTransfers; button.disabled = false; }); } function contentReady(route) { if (route === "league") return Boolean(leagueTeamTable()); if (route === "clash") { return Boolean(document.querySelector(".fed_badge") && clashTeamTable()); } return Boolean( document.querySelector(".score-cell-wrapper, a[href*='p=team']") ); } async function waitForContent(route) { if (contentReady(route)) return; await new Promise((resolve) => { const observer = new MutationObserver(() => { if (!contentReady(route)) return; observer.disconnect(); resolve(); }); observer.observe(document.documentElement, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); resolve(); }, 7e3); }); } async function initialize() { const route = routeKind(location.href); if (!route) return; injectStyles(); await waitForContent(route); if (route === "league") { const table = leagueTeamTable(); if (!table) return; mountTransferHistory(table); mountLeagueMarket( table, teamTargets(table).map((target) => ({ id: target.id, name: target.link.textContent?.trim() || target.id })) ); await Promise.allSettled([injectLeagueCurrentElo(table), injectEloDeltas(document, false)]); return; } if (route === "clash") { const latest = document.getElementById("latest-challenges") ?? document; await Promise.allSettled([ injectClashSummary(), injectClashTeamColumns(), injectEloDeltas(latest, true) ]); return; } await Promise.allSettled([ injectMatchCurrentElo(document), injectEloDeltas(document, false) ]); } var mega = typeof window === "undefined" ? void 0 : window.__mzcMega; if (mega) { mega.run("factual-insights", "1.0.0", () => { void initialize().catch((error) => { console.error("[mzcopilot factual insights]", error); }); }); } })(); /* mzcopilot bundled userscript resources */ var __mzcBundledResources = {"MAZYAR_STYLES":"/* ==UserStyle==\r\n@name MazyarStyles\r\n@namespace http://tampermonkey.net/\r\n@version 2.2\r\n@description Styles used by Mazyar userscript\r\n@copyright z7z from managerzone.com\r\n@author z7z from managerzone.com\r\n@license MIT\r\n@match https://www.managerzone.com/*\r\n@match https://test.managerzone.com/*\r\n@icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com\r\n@supportURL https://github.com/mz-ir/mazyar\r\n@downloadURL none\r\n==/UserStyle== */\r\n\r\n.mazyar-hide {\r\n display: none !important;\r\n}\r\n\r\n.mazyar-flexbox-column {\r\n display: flex;\r\n flex-direction: column;\r\n justify-content: center;\r\n align-items: center;\r\n flex-wrap: nowrap;\r\n max-height: 100%;\r\n}\r\n\r\n.mazyar-flexbox-row {\r\n display: flex;\r\n flex-direction: row;\r\n justify-content: center;\r\n align-items: center;\r\n flex-wrap: nowrap;\r\n max-width: 100%;\r\n}\r\n\r\n.mazyar-modal-table-container {\r\n width: 100%;\r\n flex: 1 1;\r\n /* body must be scrollable */\r\n overflow: auto;\r\n}\r\n\r\n.mazyar-scrollable-vertical {\r\n overflow-x: clip;\r\n overflow-y: auto;\r\n}\r\n\r\n.mazyar-resizable {\r\n resize: both;\r\n}\r\n\r\n.mazyar-modal-header {\r\n width: 100%;\r\n justify-content: space-between;\r\n text-align: left;\r\n height: 32px;\r\n text-decoration: none;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n background: #e4c800;\r\n color: #000;\r\n -moz-border-radius-topleft: 5px;\r\n -moz-border-radius-topright: 5px;\r\n -webkit-border-top-left-radius: 5px;\r\n -webkit-border-top-right-radius: 5px;\r\n border-top-left-radius: 5px;\r\n border-top-right-radius: 5px;\r\n border-color: #998504;\r\n}\r\n\r\n.mazyar-modal-body {\r\n width: 100%;\r\n flex: 1 1;\r\n max-width: fit-content;\r\n /* body must be scrollable */\r\n overflow: auto;\r\n}\r\n\r\n.mazyar-modal-footer {\r\n width: 100%;\r\n}\r\n\r\n#mazyar-modal-overlay {\r\n position: fixed;\r\n z-index: 9999;\r\n top: 0;\r\n left: 0;\r\n width: 100%;\r\n height: 100%;\r\n background-color: rgba(0, 0, 0, 0.4);\r\n}\r\n\r\n#mazyar-notebook-overlay {\r\n z-index: 9990;\r\n position: absolute;\r\n top: 0;\r\n left: 0;\r\n}\r\n\r\n#mazyar-toolbar-overlay {\r\n position: fixed;\r\n z-index: 9998;\r\n top: 40%;\r\n right: 5px;\r\n color: white;\r\n background-color: black;\r\n text-align: center;\r\n}\r\n\r\n#mazyar-deadline-overlay {\r\n position: fixed;\r\n z-index: 9997;\r\n top: 48%;\r\n right: 35px;\r\n color: white;\r\n text-align: center;\r\n padding: 0.5rem;\r\n border: 1px solid black;\r\n border-radius: 50%;\r\n}\r\n\r\n#mazyar-modal {\r\n width: fit-content;\r\n background-color: beige;\r\n border-radius: 5px;\r\n padding-bottom: 8px;\r\n min-width: 200px;\r\n max-width: 95vw;\r\n max-height: 95vh;\r\n}\r\n\r\n#mazyar-notebook {\r\n position: fixed;\r\n top: 0;\r\n left: 0;\r\n background-color: beige;\r\n border-radius: 5px;\r\n min-width: 200px;\r\n min-height: 250px;\r\n max-width: 95vw;\r\n max-height: 99vh;\r\n}\r\n\r\n.mazyar-notebook-textarea {\r\n flex: 1;\r\n resize: none;\r\n width: 90%;\r\n min-width: 180px;\r\n padding: 5px;\r\n}\r\n\r\ntable.mazyar-table {\r\n border-spacing: 0;\r\n width: auto;\r\n}\r\n\r\ntable.mazyar-table tbody td,\r\ntable.mazyar-table thead th {\r\n padding: 0.3em 0.5em;\r\n vertical-align: middle;\r\n}\r\n\r\n@media only screen and (max-device-width: 1020px) {\r\n thead.responsive-show.mazyar-responsive-show {\r\n display: table-header-group !important;\r\n }\r\n\r\n tbody tr.responsive-show.mazyar-responsive-show {\r\n display: table-row !important;\r\n }\r\n}\r\n\r\n\r\nbutton.mazyar-button {\r\n cursor: pointer;\r\n}\r\n\r\ndiv.mazyar-transfer-control-area {\r\n width: 50%;\r\n}\r\n\r\n.mazyar-dim-50 {\r\n opacity: 50%;\r\n}\r\n\r\n.mazyar-dim-60 {\r\n opacity: 60%;\r\n}\r\n\r\n.mazyar-scout-h {\r\n font-weight: bold;\r\n}\r\n\r\n.mazyar-scout-1 {\r\n color: red;\r\n}\r\n\r\n.mazyar-scout-2 {\r\n color: darkgoldenrod;\r\n}\r\n\r\n.mazyar-scout-3 {\r\n color: blue;\r\n}\r\n\r\n.mazyar-scout-4 {\r\n color: fuchsia;\r\n}\r\n\r\n.mazyar-player-comment-icon-www {\r\n vertical-align: top;\r\n}\r\n\r\n.mazyar-player-comment-icon-www span.player_icon_wrapper {\r\n text-align: center;\r\n}\r\n\r\n.mazyar-player-comment-icon-inactive {\r\n color: lightskyblue;\r\n}\r\n\r\n.mazyar-player-comment-icon-active {\r\n color: blue;\r\n}\r\n\r\n.mazyar-player-comment-textarea {\r\n margin: 0.5rem;\r\n min-width: 220px;\r\n min-height: 100px;\r\n}\r\n\r\n.mazyar-days-at-this-club {\r\n margin: 5px;\r\n padding: 3px;\r\n background-color: azure;\r\n border-radius: 5px;\r\n border: 1px solid black;\r\n text-align: center;\r\n}\r\n\r\n.mazyar-days-at-this-club.mazyar-days-entire {\r\n background-color: greenyellow;\r\n}\r\n\r\n@media only screen and (max-device-width: 1020px) {\r\n .mazyar-responsive-block {\r\n display: block !important;\r\n }\r\n}\r\n\r\n.mazyar-text-truncate {\r\n display: inline-block;\r\n width: 6rem;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n}\r\n\r\n.mazyar-settings-section-button {\r\n margin: 8px 5px 0px;\r\n padding: 5px;\r\n width: 220px;\r\n font-weight: bold;\r\n border-radius: 4px;\r\n border: solid 1px black;\r\n cursor: pointer;\r\n background-color: ghostwhite;\r\n}\r\n\r\n.mazyar-settings-section-button:hover {\r\n background-color: blanchedalmond !important;\r\n}\r\n\r\n.mazyar-cross-close-button {\r\n cursor: pointer;\r\n font-size: x-small;\r\n}\r\n\r\n.mazyar-scout-filter-info {\r\n padding: 0px 5px;\r\n /* make it stretch to fill the entire width */\r\n align-self: stretch;\r\n background-color: ghostwhite;\r\n line-height: 5px;\r\n border: 1px solid;\r\n margin: 3px 3px 0;\r\n border-radius: 5px;\r\n}\r\n\r\n.mazyar-import-sport-title {\r\n width: 100%;\r\n flex: 1 1;\r\n text-align: left;\r\n align-self: flex-start;\r\n}\r\n\r\n.mazyar-import-filter-row {\r\n width: 100%;\r\n flex: 1 1;\r\n text-align: left;\r\n align-self: flex-start;\r\n margin-left: 10px;\r\n}\r\n\r\n.mazyar-update-tip {\r\n margin: 8px 5px 0px;\r\n padding: 5px 0px;\r\n width: 220px;\r\n background-color: lightgreen;\r\n border-radius: 4px;\r\n border: 1px solid black;\r\n text-align: center;\r\n}\r\n\r\n.mazyar-update-tip>button {\r\n margin: 8px 5px 0px;\r\n padding: 5px;\r\n width: 120px;\r\n font-weight: bold;\r\n border-radius: 4px;\r\n border: solid 1px black;\r\n cursor: pointer;\r\n background-color: ghostwhite;\r\n}\r\n\r\n.mazyar-update-tip>button:hover {\r\n background-color: blanchedalmond !important;\r\n}\r\n\r\n.mazyar-update-tip>button>a {\r\n text-decoration: none;\r\n}\r\n\r\n.mazyar-disabled-icon {\r\n opacity: 0.4;\r\n pointer-events: none;\r\n cursor: not-allowed;\r\n}\r\n\r\n/* ------------------------- Animation ------------------------------ */\r\n\r\n.mazyar-donut {\r\n width: 1.7em;\r\n height: 1.7em;\r\n margin-right: 5px;\r\n border-radius: 50%;\r\n text-align: center;\r\n font-size: 1.2em;\r\n padding: 3px;\r\n background-color: yellow;\r\n color: yellow;\r\n}\r\n\r\n.mazyar-final-donut {\r\n border: rgb(213, 232, 44) solid 2px;\r\n color: inherit;\r\n padding: 0;\r\n}\r\n\r\n.mazyar-loading-donut {\r\n border-bottom-color: rgb(213, 232, 44);\r\n animation: 1.5s mazyar-donut-spin infinite linear;\r\n}\r\n\r\n@keyframes mazyar-donut-spin {\r\n to {\r\n transform: rotate(360deg);\r\n }\r\n}\r\n\r\na.mazyar-in-progress-result-green {\r\n animation: mazyar-change-result-background 3s infinite;\r\n}\r\n\r\n@keyframes mazyar-change-result-background {\r\n 50% {\r\n background-color: lightgreen;\r\n }\r\n}\r\n\r\na.mazyar-in-progress-result-gray {\r\n animation: mazyar-change-result-background-gray 8s infinite;\r\n}\r\n\r\n@keyframes mazyar-change-result-background-gray {\r\n 50% {\r\n background-color: lightgray;\r\n color: black;\r\n }\r\n}\r\n\r\n.mazyar-deadline-throb-lightgreen {\r\n animation: mazyar-throb-deadline-icon 3s infinite;\r\n}\r\n\r\n@keyframes mazyar-throb-deadline-icon {\r\n 0% {\r\n color: yellow;\r\n }\r\n\r\n 50% {\r\n color: crimson;\r\n }\r\n\r\n 100% {\r\n color: yellow;\r\n }\r\n}\r\n\r\n.mazyar-deadline-monitor-throb {\r\n animation: mazyar-throb-deadline-monitor 3s infinite;\r\n}\r\n\r\n@keyframes mazyar-throb-deadline-monitor {\r\n 50% {\r\n background-color: yellow;\r\n }\r\n}\r\n\r\n.mazyar-update-available {\r\n animation: mazyar-update-available-green 4s infinite;\r\n}\r\n\r\n@keyframes mazyar-update-available-green {\r\n 50% {\r\n color: green;\r\n }\r\n\r\n to {\r\n transform: rotate(360deg);\r\n }\r\n}\r\n","NPROGRESS_CSS":"/* Make clicks pass-through */\r\n#nprogress {\r\n pointer-events: none;\r\n}\r\n\r\n#nprogress .bar {\r\n background: #29d;\r\n\r\n position: fixed;\r\n z-index: 1031;\r\n top: 0;\r\n left: 0;\r\n\r\n width: 100%;\r\n height: 2px;\r\n}\r\n\r\n/* Fancy blur effect */\r\n#nprogress .peg {\r\n display: block;\r\n position: absolute;\r\n right: 0px;\r\n width: 100px;\r\n height: 100%;\r\n box-shadow: 0 0 10px #29d, 0 0 5px #29d;\r\n opacity: 1.0;\r\n\r\n -webkit-transform: rotate(3deg) translate(0px, -4px);\r\n -ms-transform: rotate(3deg) translate(0px, -4px);\r\n transform: rotate(3deg) translate(0px, -4px);\r\n}\r\n\r\n/* Remove these to get rid of the spinner */\r\n#nprogress .spinner {\r\n display: block;\r\n position: fixed;\r\n z-index: 1031;\r\n top: 15px;\r\n right: 15px;\r\n}\r\n\r\n#nprogress .spinner-icon {\r\n width: 18px;\r\n height: 18px;\r\n box-sizing: border-box;\r\n\r\n border: solid 2px transparent;\r\n border-top-color: #29d;\r\n border-left-color: #29d;\r\n border-radius: 50%;\r\n\r\n -webkit-animation: nprogress-spinner 400ms linear infinite;\r\n animation: nprogress-spinner 400ms linear infinite;\r\n}\r\n\r\n.nprogress-custom-parent {\r\n overflow: hidden;\r\n position: relative;\r\n}\r\n\r\n.nprogress-custom-parent #nprogress .spinner,\r\n.nprogress-custom-parent #nprogress .bar {\r\n position: absolute;\r\n}\r\n\r\n@-webkit-keyframes nprogress-spinner {\r\n 0% { -webkit-transform: rotate(0deg); }\r\n 100% { -webkit-transform: rotate(360deg); }\r\n}\r\n@keyframes nprogress-spinner {\r\n 0% { transform: rotate(0deg); }\r\n 100% { transform: rotate(360deg); }\r\n}\r\n\r\n","ongoingMatchesStyles":".mzc-mega-donor button,\r\n.mzc-mega-donor select,\r\n.mzc-mega-donor input {\r\n font: inherit;\r\n}\r\n\r\n.friendly-table,\r\n.training-history-table,\r\n.ylotp-table {\r\n width: 100%;\r\n border-collapse: collapse;\r\n}\r\n\r\n.friendly-table th,\r\n.friendly-table td,\r\n.training-history-table th,\r\n.training-history-table td,\r\n.ylotp-table th,\r\n.ylotp-table td {\r\n padding: 6px 8px;\r\n border-bottom: 1px solid rgba(127, 127, 127, 0.3);\r\n}\r\n\r\n.spinner {\r\n margin: 16px auto;\r\n}\r\n","trainingHistoryStyles":".th-overlay {\r\n position: fixed;\r\n inset: 0;\r\n z-index: 2147482000;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 24px;\r\n background: rgba(8, 15, 23, 0.76);\r\n opacity: 0;\r\n transition: opacity 0.16s ease;\r\n box-sizing: border-box;\r\n}\r\n\r\n.th-overlay.show {\r\n opacity: 1;\r\n}\r\n\r\n.th-modal {\r\n position: relative;\r\n width: min(1180px, 96vw);\r\n max-height: 92vh;\r\n overflow: hidden;\r\n border: 1px solid #82909c;\r\n border-radius: 12px;\r\n background: #f7f8fa;\r\n color: #17212b;\r\n box-shadow: 0 22px 70px rgba(0, 0, 0, 0.5);\r\n transform: translateY(8px) scale(0.99);\r\n transition: transform 0.16s ease;\r\n font: 13px/1.4 Arial, sans-serif;\r\n}\r\n\r\n.th-modal.show {\r\n transform: translateY(0) scale(1);\r\n}\r\n\r\n.th-modal-content {\r\n max-height: 92vh;\r\n overflow: auto;\r\n padding: 20px;\r\n box-sizing: border-box;\r\n}\r\n\r\n.th-modal-close {\r\n position: absolute;\r\n z-index: 2;\r\n top: 7px;\r\n right: 10px;\r\n width: 30px;\r\n height: 30px;\r\n border-radius: 50%;\r\n color: #485864;\r\n background: #eef1f4;\r\n cursor: pointer;\r\n font: 700 24px/28px Arial, sans-serif;\r\n text-align: center;\r\n}\r\n\r\n.th-modal-close:hover {\r\n color: #fff;\r\n background: #ba2d3b;\r\n}\r\n\r\n.th-title {\r\n margin: 0 38px 14px 0;\r\n color: #075a91;\r\n font-size: 21px;\r\n}\r\n\r\n.th-tab-row,\r\n.th-tab-buttons,\r\n.th-pagination-controls,\r\n.th-compare-controls,\r\n.th-scout-info {\r\n display: flex;\r\n align-items: center;\r\n flex-wrap: wrap;\r\n gap: 8px;\r\n}\r\n\r\n.th-tab-row {\r\n justify-content: space-between;\r\n margin-bottom: 14px;\r\n border-bottom: 1px solid #cfd6dc;\r\n}\r\n\r\n.th-tab-btn,\r\n.th-pagination-btn,\r\n.th-pagination-toggle,\r\n.th-action-btn,\r\n.th-btn {\r\n border: 1px solid #8b9aa7;\r\n border-radius: 6px;\r\n background: #fff;\r\n color: #24323c;\r\n cursor: pointer;\r\n font-weight: 700;\r\n}\r\n\r\n.th-tab-btn,\r\n.th-pagination-btn,\r\n.th-pagination-toggle,\r\n.th-action-btn {\r\n padding: 7px 11px;\r\n}\r\n\r\n.th-btn {\r\n margin-left: 5px;\r\n padding: 2px 6px;\r\n}\r\n\r\n.th-tab-btn.active,\r\n.th-action-btn {\r\n border-color: #0877ad;\r\n background: #0877ad;\r\n color: #fff;\r\n}\r\n\r\n.th-tab-btn:hover,\r\n.th-pagination-btn:hover:not(:disabled),\r\n.th-pagination-toggle:hover,\r\n.th-btn:hover {\r\n background: #e4f2fa;\r\n}\r\n\r\n.th-pagination-btn:disabled {\r\n opacity: 0.45;\r\n cursor: default;\r\n}\r\n\r\n.th-tab-content {\r\n display: none;\r\n}\r\n\r\n.th-tab-content.active {\r\n display: block;\r\n}\r\n\r\n.th-state-wrapper {\r\n width: 100%;\r\n}\r\n\r\n.th-all-view {\r\n display: grid;\r\n grid-template-columns: repeat(auto-fit, minmax(330px, 1fr));\r\n gap: 12px;\r\n}\r\n\r\n.th-state-col,\r\n.th-training-season,\r\n.th-transfer-info,\r\n.th-chart-wrapper {\r\n margin: 10px 0;\r\n padding: 12px;\r\n border: 1px solid #d1d8de;\r\n border-radius: 8px;\r\n background: #fff;\r\n}\r\n\r\n.th-state-col h4,\r\n.th-training-season h3 {\r\n margin: 0 0 8px;\r\n color: #075a91;\r\n}\r\n\r\n.th-state-info,\r\n.th-scout-info,\r\n.th-chips-list {\r\n margin: 7px 0;\r\n padding: 7px 9px;\r\n border-radius: 6px;\r\n background: #eef5f9;\r\n}\r\n\r\n.th-state-skills {\r\n display: grid;\r\n gap: 4px;\r\n margin-top: 10px;\r\n}\r\n\r\n.th-state-skills h5 {\r\n margin: 8px 0 3px;\r\n}\r\n\r\n.th-state-skill {\r\n display: grid;\r\n grid-template-columns: minmax(125px, 1fr) minmax(120px, 2fr) minmax(42px, auto);\r\n gap: 8px;\r\n align-items: center;\r\n padding: 4px 6px;\r\n border-bottom: 1px solid #edf0f2;\r\n}\r\n\r\n.th-skill-val {\r\n min-width: 0;\r\n overflow-wrap: anywhere;\r\n}\r\n\r\n.th-skill-change,\r\n.th-skill-increase,\r\n.th-gain-since-arrival,\r\n.th-gained-balls {\r\n color: #12813b;\r\n font-weight: 700;\r\n}\r\n\r\n.th-skill-maxed,\r\n.th-maxed-indicator {\r\n color: #b72c3b;\r\n font-weight: 700;\r\n}\r\n\r\n.th-initial-balls {\r\n color: #687680;\r\n}\r\n\r\n.th-potential-icon-hp,\r\n[class*=\"th-skill-potential-hp\"],\r\n[class*=\"th-hp-h\"] {\r\n color: #b16f00;\r\n font-weight: 700;\r\n}\r\n\r\n.th-potential-icon-lp,\r\n[class*=\"th-skill-potential-lp\"],\r\n[class*=\"th-lp-l\"] {\r\n color: #8a3fb1;\r\n font-weight: 700;\r\n}\r\n\r\n[class*=\"th-speed-s\"] {\r\n color: #0877ad;\r\n font-weight: 700;\r\n}\r\n\r\n.th-chip-display {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 4px;\r\n margin: 2px 4px;\r\n}\r\n\r\n.th-chip-icon,\r\n.th-chip-icon-table {\r\n width: 18px;\r\n height: 18px;\r\n object-fit: contain;\r\n vertical-align: middle;\r\n}\r\n\r\n.th-compare-controls textarea {\r\n min-height: 64px;\r\n flex: 1 1 320px;\r\n padding: 8px;\r\n border: 1px solid #aeb9c2;\r\n border-radius: 6px;\r\n resize: vertical;\r\n}\r\n\r\n.th-compare-results-wrapper,\r\n.th-compare-graphical-results {\r\n margin-top: 12px;\r\n}\r\n\r\n.th-compare-graphical-results {\r\n display: grid;\r\n grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));\r\n gap: 12px;\r\n}\r\n\r\n.th-chart-wrapper {\r\n min-height: 360px;\r\n}\r\n\r\n.th-compare-table {\r\n width: 100%;\r\n border-collapse: collapse;\r\n background: #fff;\r\n}\r\n\r\n.th-compare-table th,\r\n.th-compare-table td {\r\n padding: 7px 9px;\r\n border: 1px solid #d5dbe0;\r\n text-align: left;\r\n}\r\n\r\n.th-compare-table th,\r\n.th-table-subheader {\r\n background: #e7edf2;\r\n font-weight: 700;\r\n}\r\n\r\n.th-error-message,\r\n.th-no-data-message {\r\n margin: 12px 0;\r\n padding: 10px 12px;\r\n border-radius: 7px;\r\n}\r\n\r\n.th-error-message {\r\n border: 1px solid #d9949c;\r\n background: #fdebed;\r\n color: #8f1f2c;\r\n}\r\n\r\n.th-no-data-message {\r\n border: 1px solid #b9c6d0;\r\n background: #eef3f7;\r\n color: #475762;\r\n}\r\n\r\n.th-transfer-header {\r\n position: relative;\r\n font-weight: 700;\r\n}\r\n\r\n.th-transfer-currency-icon {\r\n margin-left: 5px;\r\n cursor: pointer;\r\n}\r\n\r\n.th-transfer-currency-dropdown {\r\n position: absolute;\r\n z-index: 5;\r\n display: none;\r\n min-width: 150px;\r\n padding: 6px;\r\n border: 1px solid #9aa6b2;\r\n border-radius: 6px;\r\n background: #fff;\r\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25);\r\n}\r\n\r\n.th-transfer-currency-dropdown.show {\r\n display: block;\r\n}\r\n\r\n.th-transfer-currency-dropdown ul {\r\n display: grid;\r\n grid-template-columns: repeat(4, 1fr);\r\n gap: 3px;\r\n margin: 0;\r\n padding: 0;\r\n list-style: none;\r\n}\r\n\r\n.th-transfer-currency-dropdown li {\r\n padding: 3px 5px;\r\n border-radius: 3px;\r\n cursor: pointer;\r\n}\r\n\r\n.th-transfer-currency-dropdown li:hover,\r\n.th-transfer-currency-dropdown li.selected {\r\n background: #0877ad;\r\n color: #fff;\r\n}\r\n\r\n@media (max-width: 700px) {\r\n .th-overlay {\r\n padding: 8px;\r\n }\r\n\r\n .th-modal,\r\n .th-modal-content {\r\n width: 100%;\r\n max-height: 96vh;\r\n }\r\n\r\n .th-modal-content {\r\n padding: 14px 10px;\r\n }\r\n\r\n .th-state-skill {\r\n grid-template-columns: minmax(105px, 1fr) minmax(90px, 1.4fr) auto;\r\n gap: 5px;\r\n font-size: 12px;\r\n }\r\n\r\n .th-tab-row {\r\n align-items: flex-start;\r\n }\r\n}\r\n","weeklyMatchesTrackerStyles":".mzc-mega-donor button,\r\n.mzc-mega-donor select,\r\n.mzc-mega-donor input {\r\n font: inherit;\r\n}\r\n\r\n.friendly-table,\r\n.training-history-table,\r\n.ylotp-table {\r\n width: 100%;\r\n border-collapse: collapse;\r\n}\r\n\r\n.friendly-table th,\r\n.friendly-table td,\r\n.training-history-table th,\r\n.training-history-table td,\r\n.ylotp-table th,\r\n.ylotp-table td {\r\n padding: 6px 8px;\r\n border-bottom: 1px solid rgba(127, 127, 127, 0.3);\r\n}\r\n\r\n.spinner {\r\n margin: 16px auto;\r\n}\r\n","ylotp":"/* Scoped local layout for the opponent-tactics donor.\r\n * The upstream resource disappeared; keep every generic class under its owned roots.\r\n */\r\n#interaction-modal {\r\n position: absolute;\r\n z-index: 2147482000;\r\n width: min(430px, calc(100vw - 20px));\r\n box-sizing: border-box;\r\n padding: 12px;\r\n border: 1px solid #9aa8b4;\r\n border-radius: 10px;\r\n background: #f7f9fb;\r\n color: #17212b;\r\n box-shadow: 0 14px 38px rgb(0 0 0 / 45%);\r\n font: 13px/1.4 Arial, Helvetica, sans-serif;\r\n}\r\n\r\n#interaction-modal *,\r\n#tactics-container * {\r\n box-sizing: border-box;\r\n}\r\n\r\n#interaction-modal .interaction-modal-header,\r\n#tactics-container .tactics-header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 12px;\r\n}\r\n\r\n#interaction-modal .interaction-modal-header {\r\n min-height: 24px;\r\n margin-bottom: 10px;\r\n padding-bottom: 8px;\r\n border-bottom: 1px solid #d7dee5;\r\n font-weight: 700;\r\n}\r\n\r\n#interaction-modal .settings-icon,\r\n#interaction-modal .ylotp-close-icon {\r\n display: inline-grid;\r\n width: 28px;\r\n height: 28px;\r\n place-items: center;\r\n border: 1px solid #bdc8d1;\r\n border-radius: 6px;\r\n background: #fff;\r\n color: #314454;\r\n cursor: pointer;\r\n}\r\n\r\n#interaction-modal .team-input-section {\r\n display: grid;\r\n grid-template-columns: auto minmax(90px, 1fr) minmax(130px, 1.35fr);\r\n align-items: center;\r\n gap: 8px;\r\n margin-bottom: 10px;\r\n}\r\n\r\n#interaction-modal input,\r\n#interaction-modal select,\r\n#interaction-modal button {\r\n min-height: 30px;\r\n max-width: 100%;\r\n border: 1px solid #a9b5bf;\r\n border-radius: 5px;\r\n background: #fff;\r\n color: #17212b;\r\n font: inherit;\r\n}\r\n\r\n#interaction-modal input,\r\n#interaction-modal select {\r\n width: 100%;\r\n padding: 4px 7px;\r\n}\r\n\r\n#interaction-modal button {\r\n padding: 4px 9px;\r\n cursor: pointer;\r\n}\r\n\r\n#interaction-modal button:hover,\r\n#interaction-modal button:focus-visible {\r\n border-color: #0f6da8;\r\n background: #eaf5fc;\r\n}\r\n\r\n#interaction-modal .tab-container {\r\n overflow: hidden;\r\n border: 1px solid #ccd5dc;\r\n border-radius: 7px;\r\n background: #fff;\r\n}\r\n\r\n#interaction-modal .tab-headers {\r\n display: flex;\r\n gap: 2px;\r\n padding: 4px 4px 0;\r\n background: #e8edf1;\r\n}\r\n\r\n#interaction-modal .tab-header {\r\n flex: 1 1 0;\r\n border-color: transparent;\r\n border-radius: 5px 5px 0 0;\r\n background: transparent;\r\n font-size: 12px;\r\n font-weight: 700;\r\n}\r\n\r\n#interaction-modal .tab-header.active {\r\n border-color: #aab8c3;\r\n border-bottom-color: #fff;\r\n background: #fff;\r\n color: #075d94;\r\n}\r\n\r\n#interaction-modal .tab-contents {\r\n padding: 9px;\r\n}\r\n\r\n#interaction-modal .tab-content {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 6px;\r\n}\r\n\r\n#interaction-modal .tab-content button {\r\n flex: 1 1 78px;\r\n}\r\n\r\n#interaction-modal .settings-panel {\r\n margin-top: 10px;\r\n padding: 9px;\r\n border: 1px solid #ccd5dc;\r\n border-radius: 7px;\r\n background: #eef3f6;\r\n}\r\n\r\n#interaction-modal .settings-panel label,\r\n#interaction-modal .settings-panel small {\r\n display: block;\r\n margin-bottom: 5px;\r\n}\r\n\r\n#interaction-modal .settings-panel input {\r\n width: 90px;\r\n}\r\n\r\n#interaction-modal .save-confirmation {\r\n margin-left: 8px;\r\n color: #087d38;\r\n opacity: 0;\r\n}\r\n\r\n#interaction-modal .save-confirmation.visible {\r\n opacity: 1;\r\n}\r\n\r\n#interaction-modal .error-message {\r\n margin: 8px 0;\r\n padding: 7px 9px;\r\n border-radius: 5px;\r\n background: #fce8e8;\r\n color: #9e1d1d;\r\n}\r\n\r\n#tactics-container {\r\n position: fixed;\r\n z-index: 2147482100;\r\n inset: max(64px, 7vh) 24px 24px;\r\n display: flex;\r\n width: min(1180px, calc(100vw - 48px));\r\n max-height: calc(100vh - max(88px, 10vh));\r\n margin: 0 auto;\r\n padding: 14px;\r\n flex-direction: column;\r\n overflow: hidden;\r\n border: 1px solid #8293a0;\r\n border-radius: 12px;\r\n background: #f4f7f9;\r\n color: #17212b;\r\n box-shadow: 0 18px 52px rgb(0 0 0 / 58%);\r\n font: 13px/1.4 Arial, Helvetica, sans-serif;\r\n}\r\n\r\n#tactics-container .tactics-header {\r\n flex: 0 0 auto;\r\n padding: 3px 3px 12px;\r\n border-bottom: 1px solid #ccd5dc;\r\n}\r\n\r\n#tactics-container .title-main {\r\n font-size: 17px;\r\n font-weight: 700;\r\n}\r\n\r\n#tactics-container .close-button {\r\n display: grid;\r\n width: 32px;\r\n height: 32px;\r\n padding: 0;\r\n place-items: center;\r\n border: 1px solid #a7b4be;\r\n border-radius: 7px;\r\n background: #fff;\r\n color: #263744;\r\n font-size: 22px;\r\n line-height: 1;\r\n cursor: pointer;\r\n}\r\n\r\n#tactics-container .tactics-list {\r\n display: grid;\r\n min-height: 0;\r\n padding: 14px 3px 3px;\r\n grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));\r\n gap: 12px;\r\n overflow: auto;\r\n}\r\n\r\n#tactics-container .tactic-item {\r\n position: relative;\r\n display: flex;\r\n min-width: 0;\r\n padding: 9px;\r\n align-items: center;\r\n justify-content: center;\r\n border: 2px solid #aeb9c2;\r\n border-radius: 9px;\r\n background: #fff;\r\n}\r\n\r\n#tactics-container .tactic-win {\r\n border-color: #299656;\r\n}\r\n\r\n#tactics-container .tactic-draw {\r\n border-color: #d2a72e;\r\n}\r\n\r\n#tactics-container .tactic-loss {\r\n border-color: #c84b4b;\r\n}\r\n\r\n#tactics-container .tactic-link {\r\n display: flex;\r\n min-width: 0;\r\n align-items: center;\r\n flex-direction: column;\r\n color: #124f7a;\r\n text-align: center;\r\n text-decoration: none;\r\n}\r\n\r\n#tactics-container .tactic-link canvas {\r\n display: block;\r\n width: 150px;\r\n max-width: 100%;\r\n height: 200px;\r\n}\r\n\r\n#tactics-container .tactic-link p {\r\n max-width: 100%;\r\n margin: 7px 0 0;\r\n overflow-wrap: anywhere;\r\n}\r\n\r\n#tactics-container .playstyle-status-indicator {\r\n position: absolute;\r\n z-index: 1;\r\n top: 7px;\r\n right: 7px;\r\n min-width: 18px;\r\n min-height: 18px;\r\n}\r\n\r\n#tactics-container .no-tactics-message {\r\n grid-column: 1 / -1;\r\n padding: 28px;\r\n color: #536674;\r\n text-align: center;\r\n}\r\n\r\n#spinjs-overlay {\r\n position: fixed;\r\n z-index: 2147482200;\r\n inset: 0;\r\n background: rgb(0 0 0 / 45%);\r\n}\r\n\r\n.playstyle-tooltip {\r\n position: absolute;\r\n z-index: 2147482300;\r\n display: none;\r\n max-width: min(360px, calc(100vw - 20px));\r\n padding: 9px 11px;\r\n border: 1px solid #60727f;\r\n border-radius: 7px;\r\n background: #17252f;\r\n color: #fff;\r\n box-shadow: 0 8px 22px rgb(0 0 0 / 45%);\r\n font: 12px/1.45 Arial, Helvetica, sans-serif;\r\n pointer-events: auto;\r\n}\r\n\r\n.magnifier-icon {\r\n display: inline-grid;\r\n width: 22px;\r\n height: 22px;\r\n margin-left: 5px;\r\n place-items: center;\r\n border: 1px solid #98a5af;\r\n border-radius: 5px;\r\n background: #fff;\r\n cursor: pointer;\r\n vertical-align: middle;\r\n}\r\n\r\n.magnifier-icon:hover,\r\n.magnifier-icon:focus-visible {\r\n border-color: #0f6da8;\r\n background: #eaf5fc;\r\n}\r\n\r\n#interaction-modal.fade-out,\r\n#tactics-container.fade-out {\r\n opacity: 0;\r\n pointer-events: none;\r\n}\r\n\r\n@media (max-width: 700px) {\r\n #interaction-modal {\r\n position: fixed !important;\r\n top: 10px !important;\r\n right: 10px !important;\r\n left: 10px !important;\r\n width: auto;\r\n max-height: calc(100vh - 20px);\r\n overflow: auto;\r\n }\r\n\r\n #interaction-modal .team-input-section {\r\n grid-template-columns: 1fr;\r\n }\r\n\r\n #interaction-modal .tab-headers {\r\n flex-wrap: wrap;\r\n }\r\n\r\n #interaction-modal .tab-header {\r\n flex-basis: 30%;\r\n }\r\n\r\n #tactics-container {\r\n inset: 8px;\r\n width: auto;\r\n max-height: calc(100vh - 16px);\r\n padding: 10px;\r\n }\r\n\r\n #tactics-container .tactics-list {\r\n grid-template-columns: repeat(auto-fit, minmax(155px, 1fr));\r\n }\r\n}\r\n","skillTranslationsJS":"const SKILL_TRANSLATIONS = {\r\n \"Speed\": \"Speed\",\r\n \"Stamina\": \"Stamina\",\r\n \"Play Intelligence\": \"Play Intelligence\",\r\n \"Passing\": \"Passing\",\r\n \"Shooting\": \"Shooting\",\r\n \"Heading\": \"Heading\",\r\n \"Keeping\": \"Keeping\",\r\n \"Ball Control\": \"Ball Control\",\r\n \"Tackling\": \"Tackling\",\r\n \"Aerial Passing\": \"Aerial Passing\",\r\n \"Set Plays\": \"Set Plays\",\r\n \"Velocidad\": \"Speed\",\r\n \"Resistencia\": \"Stamina\",\r\n \"Inteligencia\": \"Play Intelligence\",\r\n \"Pases\": \"Passing\",\r\n \"Remates\": \"Shooting\",\r\n \"Cabezazos\": \"Heading\",\r\n \"Atajando\": \"Keeping\",\r\n \"Control de balón\": \"Ball Control\",\r\n \"Control de balon\": \"Ball Control\",\r\n \"Entradas\": \"Tackling\",\r\n \"Pases Largos\": \"Aerial Passing\",\r\n \"Balón Parado\": \"Set Plays\",\r\n \"Balon Parado\": \"Set Plays\",\r\n \"Velocidade\": \"Speed\",\r\n \"Inteligência\": \"Play Intelligence\",\r\n \"Passe\": \"Passing\",\r\n \"Chute\": \"Shooting\",\r\n \"Cabeceio\": \"Heading\",\r\n \"Defesa\": \"Keeping\",\r\n \"Controle de Bola\": \"Ball Control\",\r\n \"Desarme\": \"Tackling\",\r\n \"Passe Longo\": \"Aerial Passing\",\r\n \"Bola Parada\": \"Set Plays\"\r\n};\r\nconst insensitiveSkillTranslations = Object.fromEntries(\r\n Object.entries(SKILL_TRANSLATIONS).map(([key, value]) => [key.toLowerCase(), value])\r\n);\r\n"}; var __mzcNativeGetResourceText = typeof GM_getResourceText === "function" ? GM_getResourceText : function () { return ""; }; GM_getResourceText = function (name) { return __mzcBundledResources[name] || __mzcNativeGetResourceText(name) || ""; }; if (typeof GM_addStyle !== "function") { globalThis.GM_addStyle = function (css) { var style = document.createElement("style"); style.textContent = css; (document.head || document.documentElement).appendChild(style); return style; }; } /* mzcopilot:module:[email protected] */ window.__mzcMega.run("legacy-player-advanced", "5.0.2", function () { (function (pako, Dexie, $, jQuery) { /* eslint-disable */ var vanGmMzModel = { language: { zh: { Name: "中文", GetPlayersHtml: "获取球员页面HTML", GetPlayersHtml2: "获取球员页面HTML(不用缓存)", Clear: "清空缓存", Save: "保存", Setting: "设置", Test: "测试", SettingTitle: "点击可对语言和XML导出进行设置", Language: "语言", XmlMode: "战术导出模式", XmlMode1: "顺序选择球员", XmlMode2: "根据位置选择球员(需提交数据到后台)", TacConf: "位置系数设置", AutoRun: "自动着色", AutoRun0: "仅打开页面时自动执行一次", AutoRun1: "随系统刷新自动着色(如果浏览器变卡请关闭此选项)", Pay: "如果你觉得好用,可通过打赏支持作者。", NotSureEx: "挂牌后属性可能有变动,不确定转会市场显示是否是真实属性,请自行甄别。<br/>属性变动时间", NotSure: "属性不确定", ManualColorTitle: "点击可手动着色 快捷键:ALT + A", ManualColor: "手动着色", Jijing: "比赛集锦", dongzuo: "球员动作", Copyxml1: "复制主队战术", Copyxml2: "复制客队战术", CopyXml: "数据已复制到剪切板", CopyXmlMsg: "战术已复制到剪切板", CopyXmlMsgError: "战术复制失败", BA_NORMAL: "BA_NORMAL", BA_WALL: "站人墙", BA_HOLD: "抱着球", BA_DOWN: "倒地", BA_HOLD_THROWIN: "界外球准备", BA_THROWIN: "界外球", BA_LEFT_FOOT_SHOT_FWD: "射门/长传(L)", BA_LEFT_FOOT_SHOT_BACK: "BA_LEFT_FOOT_SHOT_BACK", BA_LEFT_FOOT_SHOT_RIGHT: "BA_LEFT_FOOT_SHOT_RIGHT", BA_LEFT_FOOT_SHOT_LEFT: "BA_LEFT_FOOT_SHOT_LEFT", BA_RIGHT_FOOT_SHOT_FWD: "射门/长传(R)", BA_RIGHT_FOOT_SHOT_BACK: "BA_RIGHT_FOOT_SHOT_BACK", BA_RIGHT_FOOT_SHOT_RIGHT: "BA_RIGHT_FOOT_SHOT_RIGHT", BA_RIGHT_FOOT_SHOT_LEFT: "BA_RIGHT_FOOT_SHOT_LEFT", BA_LEFT_FOOT_PASS_FWD: "短传(L)", BA_LEFT_FOOT_PASS_BACK: "BA_LEFT_FOOT_PASS_BACK", BA_LEFT_FOOT_PASS_RIGHT: "BA_LEFT_FOOT_PASS_RIGHT", BA_LEFT_FOOT_PASS_LEFT: "BA_LEFT_FOOT_PASS_LEFT", BA_RIGHT_FOOT_PASS_FWD: "短传(R)", BA_RIGHT_FOOT_PASS_BACK: "BA_RIGHT_FOOT_PASS_BACK", BA_RIGHT_FOOT_PASS_RIGHT: "BA_RIGHT_FOOT_PASS_RIGHT", BA_RIGHT_FOOT_PASS_LEFT: "BA_RIGHT_FOOT_PASS_LEFT", BA_PICK_UP_BALL: "捡起球", BA_DROP_BALL: "放下球", BA_HEADER: "争顶", BA_TRIP: "丢失球权", BA_CELEBRATE: "庆祝进球", BA_GK_READY: "准备扑救", BA_GK_ACRO_LEFT: "ACRO(L)", BA_GK_ACRO_LEFT_HOLD: "ACRO_HOLD(L)", BA_GK_ACRO_RIGHT: "ACRO(R)", BA_GK_ACRO_RIGHT_HOLD: "ACRO_HOLD(R)", BA_GK_SIDESTEP_LEFT: "移动(L)", BA_GK_SIDESTEP_RIGHT: "移动(R)", BA_GK_KICK: "BA_GK_KICK", BA_GK_THROW_BALL: "BA_GK_THROW_BALL", BA_GK_STRETCH_LEFT: "BA_GK_STRETCH_LEFT", BA_GK_STRETCH_LEFT_HOLD: "BA_GK_STRETCH_LEFT_HOLD", BA_GK_STRETCH_RIGHT: "BA_GK_STRETCH_RIGHT", BA_GK_STRETCH_RIGHT_HOLD: "BA_GK_STRETCH_RIGHT_HOLD", BA_BALL_OWNER: "控球", BA_TACKLE: "上抢", BA_SLIDING_TACKLE: "BA_SLIDING_TACKLE", BA_SLIDING_TACKLE_STAND: "BA_SLIDING_TACKLE_STAND", BA_MAX: "BA_MAX", BA_MY_1001: "头球攻门(L)", BA_MY_1002: "头球攻门(R)", BA_MY_1003: "卸下球", BA_MY_1011: "上抢(成功)", BA_MY_1012: "上抢(失败)", Unknown: "未知" , training_pos: "无教练", training_camp: "进营", training_coach: "有教练", training_ycc: "调整", training_itc: "强化", training_neg: "掉球", training_unit: "格", training_unknown: "未知训练类型", training_ball_day: "离下一次涨球训练天数", training_part: "分段训练效率", training_total: "进度", training_avg: "平均", training_now: "当前训练进度", SkillsAnalysisTitle: "点击可手动着色和分析训练效率 快捷键:ALT + S", SkillsAnalysis: "训练效率分析" , sug_T0: "首选:", sug_T1: "次选:", sug_T2: "可尝试:", sug_T3: "默认:", Pos9: "后卫", Pos7: "门将", Pos10: "边锋", Pos5: "前锋", Pos4: "中场", Pos56: "中锋", Pos21: "后腰", Pos22: "边卫", Pos23: "前腰", PosScores: "位置评分", Scores: "评分", sug_Loser: "关键属性容易早死,尽早交换或开除。", sug_PRI: "训练顺序:", attrName1: "速度", attrName2: "耐力", attrName3: "意识", attrName4: "传球", attrName5: "射门", attrName6: "头球", attrName7: "守门", attrName8: "控球", attrName9: "抢断", attrName10: "传中", attrName11: "定位" , scoutReport: "球探报告" , now: "当前", future: "可达", max: "最大可能" } , en: { Name: "English", GetPlayersHtml: "Get players page", GetPlayersHtml2: "Get players page(No cache)", Clear: "Clear Cache", Save: "Save", Setting: "Setting", Test: "Test", SettingTitle: "Click setting language", Language: "Language", XmlMode: "Tactical Export Model", XmlMode1: "Sequential selection", XmlMode2: "Choose by location(need submit data to server)", TacConf: "Tactical Coefficient Setting", AutoRun: "Auto Colorable", AutoRun0: "By page loaded(Once)", AutoRun1: "By MZ Refresh", Pay: "Support the author.", NotSureEx: "Skills may change after entering the transfer market.<br/>Change time ", NotSure: "Uncertain skill", ManualColorTitle: "Click the Colorable Skill. Shortcut key : ALT + A", ManualColor: "Colorable Skill", Jijing: "Events", dongzuo: "PlayerStatus", Copyxml1: "CopyXML(home)", Copyxml2: "CopyXML(away)", CopyXml: "The data has been copied to the clipboard!", CopyXmlMsg: "The tactic was copied to the Clipboard!", CopyXmlMsgError: "Copy error!", BA_NORMAL: "Normal", BA_WALL: "Wall", BA_HOLD: "Hold", BA_DOWN: "Down", BA_HOLD_THROWIN: "HoldThrowin", BA_THROWIN: "Throwin", BA_LEFT_FOOT_SHOT_FWD: "FootShot(L)", BA_LEFT_FOOT_SHOT_BACK: "FootShotBack(L)", BA_LEFT_FOOT_SHOT_RIGHT: "FootShotRight(L)", BA_LEFT_FOOT_SHOT_LEFT: "FootShotLeft(L)", BA_RIGHT_FOOT_SHOT_FWD: "FootShot(R)", BA_RIGHT_FOOT_SHOT_BACK: "FootShotBack(R)", BA_RIGHT_FOOT_SHOT_RIGHT: "FootShotRight(R)", BA_RIGHT_FOOT_SHOT_LEFT: "FootShotLeft(R)", BA_LEFT_FOOT_PASS_FWD: "FootPass(L)", BA_LEFT_FOOT_PASS_BACK: "FootPassBack(L)", BA_LEFT_FOOT_PASS_RIGHT: "FootPassRight(L)", BA_LEFT_FOOT_PASS_LEFT: "FootPassLeft(L)", BA_RIGHT_FOOT_PASS_FWD: "FootPass(R)", BA_RIGHT_FOOT_PASS_BACK: "FootPassBack(R)", BA_RIGHT_FOOT_PASS_RIGHT: "FootPassRight(R)", BA_RIGHT_FOOT_PASS_LEFT: "FootPassLeft(R)", BA_PICK_UP_BALL: "PickUpBall", BA_DROP_BALL: "DropBall", BA_HEADER: "Header", BA_TRIP: "Trip", BA_CELEBRATE: "Celebrate", BA_GK_READY: "GkReady", BA_GK_ACRO_LEFT: "GkAcroLeft", BA_GK_ACRO_LEFT_HOLD: "GkAcroLeftHold", BA_GK_ACRO_RIGHT: "GkAcroRight", BA_GK_ACRO_RIGHT_HOLD: "GkAcroRightHold", BA_GK_SIDESTEP_LEFT: "GkSidestepLeft", BA_GK_SIDESTEP_RIGHT: "GkSidestepRight", BA_GK_KICK: "GkKick", BA_GK_THROW_BALL: "GkThrowBall", BA_GK_STRETCH_LEFT: "GkStretchLeft", BA_GK_STRETCH_LEFT_HOLD: "GkStretchLeftHold", BA_GK_STRETCH_RIGHT: "GkStretchRight", BA_GK_STRETCH_RIGHT_HOLD: "GkStretchRightHold", BA_BALL_OWNER: "BallOwner", BA_TACKLE: "Tackle", BA_SLIDING_TACKLE: "SlidingTackle", BA_SLIDING_TACKLE_STAND: "SlidingTackleStand", BA_MAX: "Max", BA_MY_1001: "HeadGoal(L)", BA_MY_1002: "HeadGoal(R)", BA_MY_1003: "StopTheBall", BA_MY_1011: "Tackle(Success)", BA_MY_1012: "Tackle(Fail)", Unknown: "Unknown" , training_pos: "No coach", training_camp: "CAMP", training_coach: "Coach", training_ycc: "Ycc", training_itc: "Itc", training_neg: "Neg", training_unit: "↑", training_unknown: "Unknown training", training_ball_day: "Next ball days", training_part: "Training efficiency", training_total: "Total", training_avg: "Avg", training_now: "Now", SkillsAnalysisTitle: "Colorable Skill and Analysis of Training Efficiency Shortcut key :ALT + S", SkillsAnalysis: "Colorable By Graphs" , sug_T0: "First plan:", sug_T1: "Second plan:", sug_T2: "May try:", sug_T3: "Default:", Pos9: "CB", Pos7: "GK", Pos10: "WF", Pos5: "ST", Pos4: "MF", Pos56: "CF", Pos21: "DM", Pos22: "WB", Pos23: "AMF", PosScores: "Tactics score", Scores: "Scores", sug_Loser: "Key skills too low.Exchange or dismiss as soon as possible.", sug_PRI: "Training order:", attrName1: "Speed", attrName2: "Stamina", attrName3: "Play Intelligence", attrName4: "Passing", attrName5: "Shooting", attrName6: "Heading", attrName7: "Keeping", attrName8: "Ball Control", attrName9: "Tackling", attrName10: "Aerial Passing", attrName11: "Set Plays" , scoutReport: "Scout Report", now: "now", future: "attainable", max: "max" } , es: { Name: "Español", GetPlayersHtml: "Página de jugadores", GetPlayersHtml2: "Página de jugadores(sin caché)", Clear: "Vaciar el caché", Save: "Guardar", Setting: "Ajustes", Test: "Test", SettingTitle: "Haga clic en configuración Idioma", Language: "Idioma", XmlMode: "Tactical Export Model", XmlMode1: "Secuencia de selección", XmlMode2: "Elegir por locación (necesita suministrar datos al servidor)", TacConf: "Tactical Coefficient Setting", AutoRun: "Auto Coloreable", AutoRun0: "Por página cargada (sólo una vez)", AutoRun1: "Por recargar MZ", Pay: "Apoya al autor.", NotSureEx: "Las skills pueden cambiar después de ingresar al mercado. <br/> Cambiar hora ", NotSure: "Skill incierta", ManualColorTitle: "Haga clic en la skill para colorear. Acceso directo: ALT + A", ManualColor: "Color de la skill", Jijing: "Eventos", dongzuo: "PlayerStatus", Copyxml1: "CopiarXML(local)", Copyxml2: "CopiarXML(visitante)", CopyXml: "Los datos se han copiado al portapapeles", CopyXmlMsg: "La táctica fue copiada al portapapeles", CopyXmlMsgError: "¡Error al copiar!", BA_NORMAL: "Normal", BA_WALL: "Wall", BA_HOLD: "Hold", BA_DOWN: "Down", BA_HOLD_THROWIN: "HoldThrowin", BA_THROWIN: "Throwin", BA_LEFT_FOOT_SHOT_FWD: "FootShot(L)", BA_LEFT_FOOT_SHOT_BACK: "FootShotBack(L)", BA_LEFT_FOOT_SHOT_RIGHT: "FootShotRight(L)", BA_LEFT_FOOT_SHOT_LEFT: "FootShotLeft(L)", BA_RIGHT_FOOT_SHOT_FWD: "FootShot(R)", BA_RIGHT_FOOT_SHOT_BACK: "FootShotBack(R)", BA_RIGHT_FOOT_SHOT_RIGHT: "FootShotRight(R)", BA_RIGHT_FOOT_SHOT_LEFT: "FootShotLeft(R)", BA_LEFT_FOOT_PASS_FWD: "FootPass(L)", BA_LEFT_FOOT_PASS_BACK: "FootPassBack(L)", BA_LEFT_FOOT_PASS_RIGHT: "FootPassRight(L)", BA_LEFT_FOOT_PASS_LEFT: "FootPassLeft(L)", BA_RIGHT_FOOT_PASS_FWD: "FootPass(R)", BA_RIGHT_FOOT_PASS_BACK: "FootPassBack(R)", BA_RIGHT_FOOT_PASS_RIGHT: "FootPassRight(R)", BA_RIGHT_FOOT_PASS_LEFT: "FootPassLeft(R)", BA_PICK_UP_BALL: "Toma la pelota", BA_DROP_BALL: "Tira la pelota al césped", BA_HEADER: "Cabezazo", BA_TRIP: "Trip", BA_CELEBRATE: "Celebración", BA_GK_READY: "Arquero preparado", BA_GK_ACRO_LEFT: "GkAcroLeft", BA_GK_ACRO_LEFT_HOLD: "GkAcroLeftHold", BA_GK_ACRO_RIGHT: "GkAcroRight", BA_GK_ACRO_RIGHT_HOLD: "GkAcroRightHold", BA_GK_SIDESTEP_LEFT: "Arquero da un paso a la izquierda", BA_GK_SIDESTEP_RIGHT: "Arquero da un paso a la derecha", BA_GK_KICK: "Arquero da una patada", BA_GK_THROW_BALL: "GkThrowBall", BA_GK_STRETCH_LEFT: "GkStretchLeft", BA_GK_STRETCH_LEFT_HOLD: "GkStretchLeftHold", BA_GK_STRETCH_RIGHT: "GkStretchRight", BA_GK_STRETCH_RIGHT_HOLD: "GkStretchRightHold", BA_BALL_OWNER: "Con la pelota en los pies", BA_TACKLE: "Entrada", BA_SLIDING_TACKLE: "Barrida", BA_SLIDING_TACKLE_STAND: "SlidingTackleStand", BA_MAX: "Max", BA_MY_1001: "HeadGoal(L)", BA_MY_1002: "HeadGoal(R)", BA_MY_1003: "Frena la pelota", BA_MY_1011: "Entrada(acertada)", BA_MY_1012: "Entrada(fallida)", Unknown: "Desconocido" , training_pos: "Sin entrenador", training_camp: "CAMP", training_coach: "Coach", training_ycc: "YCC", training_itc: "ITC", training_neg: "Neg", training_unit: "↑", training_unknown: "Entrenamiento desconocido", training_ball_day: "Días para ganar una pelotita", training_part: "Eficacia de entrenamiento", training_total: "Total", training_avg: "Avg", training_now: "Ahora", SkillsAnalysisTitle: "Skills y análisis de eficacia de entrenamiento coloreables al presionar: ALT + S", SkillsAnalysis: "Coloreable por gráficos" , sug_T0: "Primera opción:", sug_T1: "Segunda opción:", sug_T2: "Podrías intentar:", sug_T3: "Predeterminado:", Pos9: "Centro defensor", Pos7: "Arquero", Pos10: "Delantero lateral", Pos5: "Segundo delantero", Pos4: "Centrocampista", Pos56: "Miediocampista/Anchor", Pos21: "DM", Pos22: "WB", Pos23: "AMF", PosScores: "Puntuación táctica", Scores: "Puntuación", sug_Loser: "Habilidades clave demasiado bajas. Intercambialo lo más antes posible.", sug_PRI: "Orden de entrenamiento:", attrName1: "Velocidad", attrName2: "Resistencia", attrName3: "Inteligencia", attrName4: "Pases", attrName5: "Remates", attrName6: "Cabezazos", attrName7: "Atajando", attrName8: "Control de balón", attrName9: "Entradas", attrName10: "Pases Largos", attrName11: "Balón Parado" , scoutReport: "REPORTE DE SCOUTEO", now: "ahora", future: "realizable", max: "máximo" } , br: { Name: "Português", GetPlayersHtml: "Página de jogadores", GetPlayersHtml2: "Página de jugadores(sem cache)", Clear: "Esvaziar o cache", Save: "Salvar", Setting: "Configurações", Test: "Teste", SettingTitle: "Clique para configurar linguagem", Language: "Linguagem", XmlMode: "Exportar Modelo Tático", XmlMode1: "Seleção Sequencial", XmlMode2: "Escolha localização (precisa submeter dados ao servidor)", TacConf: "Configuração de Coeficiente Tático", AutoRun: "Auto Colorir", AutoRun0: "Por página carregada(Uma vez)", AutoRun1: "Recarregada do MZ", Pay: "Apoie o autor.", NotSureEx: "Habilidades podem mudar depois de entrar no mercado de transferências.<br/>Hora da mudança ", NotSure: "Habilidade incerta", ManualColorTitle: "Clique na habilidade colorida. Tecla de atalho: ALT + A", ManualColor: "Habilidade colorida", Jijing: "Eventos", dongzuo: "Status do jogador", Copyxml1: "Copiar XML(mandante)", Copyxml2: "Copiar XML(visitante)", CopyXml: "Os dados foram copiados para a área de transferência!", CopyXmlMsg: "A tática foi copiada para a área de transferências!", CopyXmlMsgError: "Erro na cópia!", BA_NORMAL: "Normal", BA_WALL: "Wall", BA_HOLD: "Hold", BA_DOWN: "Down", BA_HOLD_THROWIN: "HoldThrowin", BA_THROWIN: "Throwin", BA_LEFT_FOOT_SHOT_FWD: "FootShot(L)", BA_LEFT_FOOT_SHOT_BACK: "FootShotBack(L)", BA_LEFT_FOOT_SHOT_RIGHT: "FootShotRight(L)", BA_LEFT_FOOT_SHOT_LEFT: "FootShotLeft(L)", BA_RIGHT_FOOT_SHOT_FWD: "FootShot(R)", BA_RIGHT_FOOT_SHOT_BACK: "FootShotBack(R)", BA_RIGHT_FOOT_SHOT_RIGHT: "FootShotRight(R)", BA_RIGHT_FOOT_SHOT_LEFT: "FootShotLeft(R)", BA_LEFT_FOOT_PASS_FWD: "FootPass(L)", BA_LEFT_FOOT_PASS_BACK: "FootPassBack(L)", BA_LEFT_FOOT_PASS_RIGHT: "FootPassRight(L)", BA_LEFT_FOOT_PASS_LEFT: "FootPassLeft(L)", BA_RIGHT_FOOT_PASS_FWD: "FootPass(R)", BA_RIGHT_FOOT_PASS_BACK: "FootPassBack(R)", BA_RIGHT_FOOT_PASS_RIGHT: "FootPassRight(R)", BA_RIGHT_FOOT_PASS_LEFT: "FootPassLeft(R)", BA_PICK_UP_BALL: "Pega a bola", BA_DROP_BALL: "Deixa a bola", BA_HEADER: "Cabeceio", BA_TRIP: "Trip", BA_CELEBRATE: "Celebração", BA_GK_READY: "Goleiro pronto", BA_GK_ACRO_LEFT: "GkAcroLeft", BA_GK_ACRO_LEFT_HOLD: "GkAcroLeftHold", BA_GK_ACRO_RIGHT: "GkAcroRight", BA_GK_ACRO_RIGHT_HOLD: "GkAcroRightHold", BA_GK_SIDESTEP_LEFT: "Goleiro dá passo à esquerda", BA_GK_SIDESTEP_RIGHT: "Goleiro dá passo à direita", BA_GK_KICK: "Goleiro chuta", BA_GK_THROW_BALL: "GkThrowBall", BA_GK_STRETCH_LEFT: "GkStretchLeft", BA_GK_STRETCH_LEFT_HOLD: "GkStretchLeftHold", BA_GK_STRETCH_RIGHT: "GkStretchRight", BA_GK_STRETCH_RIGHT_HOLD: "GkStretchRightHold", BA_BALL_OWNER: "Com a bola nos pés", BA_TACKLE: "Desarme", BA_SLIDING_TACKLE: "Carrinho", BA_SLIDING_TACKLE_STAND: "SlidingTackleStand", BA_MAX: "Max", BA_MY_1001: "HeadGoal(L)", BA_MY_1002: "HeadGoal(R)", BA_MY_1003: "Pára a bola", BA_MY_1011: "Desarme(Sucesso)", BA_MY_1012: "Desarme(Falha)", Unknown: "Desconhecido" , training_pos: "Sem treinador", training_camp: "CAMP", training_coach: "Coach", training_ycc: "Ycc", training_itc: "Itc", training_neg: "Neg", training_unit: "↑", training_unknown: "Treinamento desconhecido", training_ball_day: "Dias para ganhar bola", training_part: "Eficiência de treinamento", training_total: "Total", training_avg: "Avg", training_now: "Agora", SkillsAnalysisTitle: "Tecla de atalho de Habilidades coloridas e Análise de Eficiência de treinamento: ALT + S", SkillsAnalysis: "Colorir por gráficos" , sug_T0: "Primeira opção:", sug_T1: "Segunda opção:", sug_T2: "Pode tentar:", sug_T3: "Padrão:", Pos9: "Zagueiro central", Pos7: "Goleiro", Pos10: "Ponta", Pos5: "Segundo atacante", Pos4: "Meia Central", Pos56: "Meio-campo/Volante", Pos21: "DM", Pos22: "WB", Pos23: "AMF", PosScores: "Pontuação tática", Scores: "Pontuação", sug_Loser: "Habilidades principais muito baixas. Troque ou dispense o mais breve possível.", sug_PRI: "Ordem de treinamento:", attrName1: "Velocidade", attrName2: "Resistência", attrName3: "Inteligência", attrName4: "Passe Curto", attrName5: "Chute", attrName6: "Cabeceio", attrName7: "Defesa a Gol", attrName8: "Controle de Bola", attrName9: "Desarme", attrName10: "Passe Longo", attrName11: "Bola Parada" , scoutReport: "Relatório de Observador", now: "agora", future: "realizável", max: "máximo" } } , scoutLocList: { "9": { "Prop": { "1": 0, "2": 0, "9": 1, "10": 2, "8": 2, "3": 2, "4": 2, "6": 2 }, "Order": [1, 2, 9, 10, 8, 3, 4, 6], "CampKey": "9" }, "7": { "Prop": { "2": 0, "7": 1, "3": 1, "1": 1, "10": 2, "11": 2, "8": 2 }, "Order": [2, 7, 3, 1, 10, 11, 8], "CampKey": "7" }, "10": { "Prop": { "1": 0, "2": 0, "10": 1, "8": 1, "9": 2, "4": 2, "5": 2, "6": 2 }, "Order": [1, 2, 10, 8, 9, 4, 5, 6], "CampKey": "10" }, "5": { "Prop": { "1": 0, "2": 0, "5": 1, "8": 1, "6": 1, "3": 2, "4": 2, "10": 2, "9": 2 }, "Order": [1, 2, 5, 8, 6, 3, 4, 10, 9], "CampKey": "5" }, "4": { "Prop": { "1": 0, "2": 0, "4": 1, "8": 1, "3": 1, "10": 2, "9": 2, "5": 2, "6": 2 }, "Order": [1, 2, 4, 8, 3, 10, 9, 5, 6], "CampKey": "4" }, "5,6": { "Prop": { "2": 0, "1": 0, "5": 1, "6": 1, "8": 1, "3": 2, "4": 2, "10": 2, "9": 2 }, "Order": [2, 1, 5, 8, 6, 3, 4, 10, 9], "CampKey": "56" } } , OKeys: ["9", "7", "10", "5", "4", "5,6"] , CTable: function () { //key value this.data = {}; //keys this.keys = new Array(); this.addData = function (key, value) { if (this.data[key] == undefined) { this.keys.push(key); this.keys.sort(function (a, b) { return a - b; }); } this.data[key] = value; }; this.getVal = function (i) { if (this.data[i] == undefined) { // } else { return this.data[i]; } }; this.GetX = function (i) { let rv = this.getVal(i); rv = Math.floor(rv * 214 / 1000) - 3; return rv; }; this.GetY = function (i) { let rv = this.getVal(i); rv = Math.floor(rv * 328 / 1000) - 1; return rv; }; } , mzcamp: function () { this.data = {}; this.name = null; this.keys = new Array(); this.index = 0; this.add = function (begin, end) { if (end > begin + 2937600000) { end = begin + 2937600000; } if (this.data[begin] == undefined) { this.keys.push(begin); this.keys.sort(function (a, b) { return a - b; }); } this.data[begin] = { begin: begin, end: end }; }; this.getItem = function (begin) { return this.data[begin]; }; this.resetIndex = function () { this.index = 0; }; this.inYTC = function (x) { while (this.index < this.keys.length) { let item = this.data[this.keys[this.index]]; if (x < item.begin) { return false; } else if (x <= item.end) { if (/\d/.test(item.name)) { return false; } return true; } if (x > item.end) { this.index++; } } return false; }; } , playerTrainingBySkill: function () { //涨球时间 this.ballDay = 0; //训练统计 this.stat = new vanGmMzModel.trainingStat(); //当前球数 this.skill = 0; //最后一次训练 this.lt = 0; } , trainingStat: function () { //所有 不包含掉球 this.all = new vanGmMzModel.trainingDay(); ////普通训练 无教练 //this.pos = new vanGmMzModel.trainingDay(); ////强化营 //this.itc = new vanGmMzModel.trainingDay(); ////调整营 //this.ycc = new vanGmMzModel.trainingDay(); ////掉球 //this.neg = new vanGmMzModel.trainingDay(); ////理疗 //this.physio = new vanGmMzModel.trainingDay(); ////有教练 //this.coach = new vanGmMzModel.trainingDay(); ////训练营 //this.camp = new vanGmMzModel.trainingDay(); //对某类型加1 this.add = function (type, tn) { if (!this[type]) { this[type] = new vanGmMzModel.trainingDay(); } this[type][tn] += 1; }; this.getSum = function (canOver100) { let ret = 0; if (this.neg) { ret = this.all.getSum() - this.neg.getSum(); } else { ret = this.all.getSum(); } if (!canOver100) { if (ret >= 100) { ret = 99.99; } } return ret; }; this.getAvg = function () { //let ret = this.getSum() / (this.t1 * 1 + this.t2 * 1 + this.t3 * 1 + this.t4 * 1 + this.t5 * 1 + this.t6 * 1 + this.t7 * 1 + this.t8 * 1 + this.t9 * 1 + this.t10 * 1); //ret = parseFloat(ret).toFixed(1); //return ret; if (this.neg) { return -this.neg.getAvg(); } else { return this.all.getAvg(); } }; this.getTnText = function (now_language) { let str = ""; if (this.pos) { str += " " + now_language.training_pos + this.pos.getTnText(now_language); } if (this.camp) { str += " " + now_language.training_camp + this.camp.getTnText(now_language); } if (this.coach) { str += " " + now_language.training_coach + this.coach.getTnText(now_language); } if (this.ycc) { str += " " + now_language.training_ycc + this.ycc.getTnText(now_language); } if (this.itc) { str += " " + now_language.training_itc + this.itc.getTnText(now_language); } if (this.neg) { str += " " + now_language.training_neg + this.neg.getTnText(now_language); } return str; }; //this.getAvgAndText = function () { // //let ret = this.getSum() / (this.t1 * 1 + this.t2 * 1 + this.t3 * 1 + this.t4 * 1 + this.t5 * 1 + this.t6 * 1 + this.t7 * 1 + this.t8 * 1 + this.t9 * 1 + this.t10 * 1); // //ret = parseFloat(ret).toFixed(1); // //return ret; // let ret = this.getAvg(); // let str = ""; // if (ret <= 0.4) { // str = "1格"; // } // else if (ret > 0.4 && ret <= 1.1) { // str = "2格"; // } // else if (ret > 0.4 && ret <= 2.1) { // str = "2格"; // } // this.t1 * 0.4 * 1 // + this.t2 * 0.55 * 2 // + this.t3 * 0.7 * 3 // + this.t4 * 0.85 * 4 // + this.t5 * 1 * 5 // + this.t6 * 1.15 * 6 // + this.t7 * 1.3 * 7 // + this.t8 * 1.45 * 8 // + this.t9 * 1.6 * 9 // + this.t10 * 1.75 * 10; //}; this.getDayByAvg = function (avg) { let ret = (100 - this.getSum()) / avg; ret = parseFloat(ret).toFixed(1); return ret; }; this.getDay = function () { if (this.coach) { return this.getDayByAvg(this.coach); } else if (this.pos) { return this.getDayByAvg(this.pos); } return undefined; }; } , trainingDay: function () { this.t1 = 0; this.t2 = 0; this.t3 = 0; this.t4 = 0; this.t5 = 0; this.t6 = 0; this.t7 = 0; this.t8 = 0; this.t9 = 0; this.t10 = 0; this.getSum = function () { let ret = this.t1 * 0.645 * 1 + this.t2 * 0.55 * 2 + this.t3 * 0.7 * 3 + this.t4 * 0.85 * 4 + this.t5 * 0.96 * 5 + this.t6 * 1.111 * 6 + this.t7 * 1.3 * 7 + this.t8 * 1.6 * 8 + this.t9 * 2.02 * 9 + this.t10 * 2.4 * 10; ret = parseFloat(ret).toFixed(1); return ret; }; this.getAvg = function () { let ret = this.getSum() / (this.t1 * 1 + this.t2 * 1 + this.t3 * 1 + this.t4 * 1 + this.t5 * 1 + this.t6 * 1 + this.t7 * 1 + this.t8 * 1 + this.t9 * 1 + this.t10 * 1); ret = parseFloat(ret).toFixed(1); return ret; }; this.getTnText = function (now_language) { let str = ""; if (this.t1 > 0) { str += " 1" + now_language.training_unit + "(" + this.t1 + ")"; } if (this.t2 > 0) { str += " 2" + now_language.training_unit + "(" + this.t2 + ")"; } if (this.t3 > 0) { str += " 3" + now_language.training_unit + "(" + this.t3 + ")"; } if (this.t4 > 0) { str += " 4" + now_language.training_unit + "(" + this.t4 + ")"; } if (this.t5 > 0) { str += " 5" + now_language.training_unit + "(" + this.t5 + ")"; } if (this.t6 > 0) { str += " 6" + now_language.training_unit + "(" + this.t6 + ")"; } if (this.t7 > 0) { str += " 7" + now_language.training_unit + "(" + this.t7 + ")"; } if (this.t8 > 0) { str += " 8" + now_language.training_unit + "(" + this.t8 + ")"; } if (this.t9 > 0) { str += " 9" + now_language.training_unit + "(" + this.t9 + ")"; } if (this.t10 > 0) { str += " 10" + now_language.training_unit + "(" + this.t10 + ")"; } return str; }; } , mzreg: { td_skill_val: /(\d+)/, img_val: /(\d+)\.gif/, playerMax: /trainingField.players\s*=\s*({.+})/, playerId: /player_id_(\d+)/, bar_itc: /bar_itc_(\d+)/, bar_ycc: /bar_ycc_(\d+)/, bar_pos: /bar_pos_(\d+)/, bar_neg: /bar_neg_(\d+)/, trainingType: /&t=([^)]+)/, data2d_url: /matchviewer\/media/, shortlist_url: /\/?p=shortlist/, ruok_url: /\/?p=team&tid=572357/, playerId_tac: /pid=(\d+)/, matchResult: /\/?p=match&sub=result&mid=(\d+)/, nocache: /nocache-\d+/ //data2d_url: /matchviewer\/getMatchFiles.php\?type=data&mid=\d+/ }, mzImg: { g: "data:image/gif;base64,R0lGODlhDAAKAJEDAP///8zM/wAA/////yH5BAEAAAMALAAAAAAMAAoAAAIk3CIpYZ0BABJtxvjMgojTIVwKpl0dCQbQJX3T+jpLNDXGlDUFADs=", r: "data:image/gif;base64,R0lGODlhDAAKAJEDAP////8AAMyZmf///yH5BAEAAAMALAAAAAAMAAoAAAIk3BQZYp0CAAptxvjMgojTEVwKpl0dCQrQJX3T+jpLNDXGlDUFADs=", b: "data:image/gif;base64,R0lGODlhDAAKAJEDAP///5mZmQAAAP///yH5BAEAAAMALAAAAAAMAAoAAAIk3CIpYZ0BABJtxvjMgojTIVwKpl0dCQbQJX3T+jpLNDXGlDUFADs=", p: "data:image/gif;base64,R0lGODlhDAAKAJEDAP///5lm/5kzzP///yH5BAEAAAMALAAAAAAMAAoAAAIk3CIpYZ0BABJtxvjMgojTIVwKpl0dCQbQJX3T+jpLNDXGlDUFADs=", x: "data:image/gif;base64,R0lGODlhBgAKAJEDAJnMZpmZmQAAAP///yH5BAEAAAMALAAAAAAGAAoAAAIRXCRhApAMgoPtVXXS2Lz73xUAOw==" }, hImg: { g: "data:image/gif;base64,R0lGODlhDAAKALMNAOnt/+Xr/9ri/6/A/6G1/73L/52x/8/Z/4Wf/32Z/1x9/0Rr/x9N/////wAAAAAAACH5BAEAAA0ALAAAAAAMAAoAAAQwsDXD2FJB6sot0UnHLYckdoJ5VmmzWoi0nAuxSIEyW0qxJBrFAAAYKCoaSYgD1EQAADs=", r: "data:image/gif;base64,R0lGODlhDAAKANUkAOXq//8pKunt//ZTVPz19dXZ+tzk/+NAV+bl+Ojs//4wMeWkreXp/f4tLvRMT+Dm/+Xr/7lra/4vMepKSv7///ZRUvz8/tmHhs/Z/+xKSfVeYNCJkfhFRPUxOuBUZvRsb9ri//39//hDQv8oKf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACQALAAAAAAMAAoAAAZCQBLpMhqJMgiLkEQoOkeDwnLzdIowwuoTlNUWDRSSF/oIkUQBZ6BRAWiEkIlopPgsEhzPMgIQCBgOEiNLQh1PB4RBADs=", b: "data:image/gif;base64,R0lGODlhDAAKAMQUALW1teLi4oCAgMDAwB0dHevr6+3t7aCgoDMzM9nZ2bOzs0pKSszMzC0tLX9/f19fXz8/P5qamhISEgAAAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABQALAAAAAAMAAoAAAU1IEUpkgQ5hahObNsAatTOTyLOMxHcOKvzvcdB1Oi9FqKCA1I6MR4RlWBgMAwEiIlKJGtFVSEAOw==", p: "data:image/gif;base64,R0lGODlhDAAKALMNAOnt/+Xr/9ri/6/A/6G1/73L/52x/8/Z/4Wf/32Z/1x9/4tQ/30+/////wAAAAAAACH5BAEAAA0ALAAAAAAMAAoAAAQwsDXD2FJB6sot0UnHLYckdoJ5VmmzWoi0nAuxSIEyW0qxJBrFAAAYKCoaSYgD1EQAADs=", x: "data:image/gif;base64,R0lGODlhBgAKAJEDAJnMZpmZmQAAAP///yH5BAEAAAMALAAAAAAGAAoAAAIRXCRhApAMgoPtVXXS2Lz73xUAOw==" } , //以下为2D比赛辅助 MatchEvent: function () { this.data = new Array(); this.setAllPlayerEvent = function (team, tc) { for (let i = 0; i < tc.length; i++) { this.data.push(tc[i]); } for (let i = 0; i < team.m_players.length; i++) { let len = team.m_players[i].m_events.getLength(); for (let j = 0; j < len; j++) { this.data.push(team.m_players[i].m_events.at(j)); } } }; this.Sort = function () { this.data.sort(function (a, b) { return a.m_frame - b.m_frame; }); }; } , OutOfPlay: function () { this.data = new Array(); this.add = function (begin, end) { this.data.push({ begin: begin, end: end }); }; this.resetIndex = function () { this.index = 0; }; this.notin = function (frame) { while (this.index < this.data.length) { let item = this.data[this.index]; if (frame < item.begin) { return true; } else if (frame <= item.end) { return false; } if (frame > item.end) { this.index++; } } return true; }; this.Sort = function () { this.data.sort(function (a, b) { return a.begin - b.begin; }); } } }; var vanGmMz = { now_sport: "soccer", now_language: vanGmMzModel.language.en , pmax: {}, tacP: {}, tacCof: {}, trainingInfo: {}, vv: "", getMax: function (callback) { vple.ajax( "/?p=training&sport=" + vanGmMz.now_sport, function (data) { let result = data.match(vanGmMzModel.mzreg.playerMax); if (result) { vanGmMz.pmax = JSON.parse(result[1]); callback(vanGmMz.pmax); } else { return true; } }); return false; } , setSrc: function (transfer, img, skill, maxed, skillBallDay, pid, k) { img.skill = skill; img.maxed = maxed; let old = true; let p_tr = $(img).parents("tr:first"); if (p_tr.length == 0 || p_tr.attr("class")) { p_tr = $(img).nextAll(".skill_exact:first"); old = false; } if (skill > 0) { let flag_exit = false; if (transfer && skillBallDay) { if (new Date().getTime() - skillBallDay < 345600000) { vanGmMz.getTrainingGraphsBySkill_id(pid, k, function (data) { let result = data.match(new RegExp('{"x":' + skillBallDay + ',"y":(\\d+),[^}]*"marker"')); if (result && result.length) { $(img).parents("tr:first").find("td.skillval").html("(<span class=\"" + (maxed === "red" ? "maxed" : "") + "\">" + result[1] + "</span>)"); vanGmMz.setSrc(false, img, parseInt(result[1]), maxed, false, pid, k); flag_exit = true; return true; } else { return false; } }); //$(img).parent().find("span").remove(); //$(img).parent().append("<span class=\"help_button_placeholder\"><a class=\"help_button\" href=\"#\" onclick=\"showHelpLayer('" + now_language.NotSureEx + new Date(skillBallDay).toLocaleString() + "', '" + now_language.NotSure + "', true); return false\"><span class=\"help_button_wrapper\"><span class=\"help_button_text\">?</span></span></a></span>"); } } if (flag_exit) { return; } if (pid && vanGmMz.trainingInfo[pid][k]) { let extmp = p_tr.find(".skill_exact2"); if (extmp.length > 0) { extmp.remove(); } let sum = 0; if (vanGmMz.trainingInfo[pid][k].neg && vanGmMz.trainingInfo[pid][k].neg[skill + 1]) { sum = vanGmMz.trainingInfo[pid][k].neg[skill + 1].stat.getSum(); } else if (vanGmMz.trainingInfo[pid][k][skill]) { sum = vanGmMz.trainingInfo[pid][k][skill].stat.getSum(); } if (old) { p_tr.append("<td class='skill_exact2'><div><span id=" + pid + "_" + k + "_" + skill + " class='skillval skill_exact_van'>" + sum + "%</span></div></td>"); } else { p_tr.append("<div class='skill_exact2 clearfix' style='display:inline-block;'><div><span id=" + pid + "_" + k + "_" + skill + " class='skillval skill_exact_van'>" + sum + "%</span></div></div>"); } } if (img.isYtc) { p_tr.children().eq(0).addClass("gm_ytc"); } } let strdiv; if (old) { strdiv = "<div class='skill' style='font-size:0;padding: 0 0 0 4px;'>"; } else { strdiv = "<div class='skilladd ignore-fluid'>"; } let tmpImg = vanGmMzModel.mzImg; if (vanGmMz.now_sport != "soccer") { tmpImg = vanGmMzModel.hImg; } for (let i = 0; i < skill; i++) { if (maxed === "red") { strdiv += "<img src='" + tmpImg.r + "'>"; } else if (maxed === "green") { strdiv += "<img src='" + tmpImg.g + "'>"; } else { strdiv += "<img src='" + tmpImg.b + "'>"; } } if (/blevel_/.test(img.src)) { strdiv += "<img src='" + tmpImg.x + "'>"; } strdiv += "</div>"; $(img).hide(); if (old) { $(img).parent().find("div").remove(); } else { $(img).nextAll("div.skilladd:first").remove(); } $(img).after(strdiv); } , showMax: function (GraphsType) { let players = $(".playerContainer"); for (let i = 0; i < players.length; i++) { let pdom = players.eq(i); let pid = pdom.html().match(vanGmMzModel.mzreg.playerId)[1]; let player = vanGmMz.pmax[pid]; let imgs = pdom.find("div.player-skills").find("img.ignore-fluid"); if (imgs.length == 0) { imgs = pdom.find("img.skill"); } if (GraphsType == 0 && player) { this.setPlayerImgs(imgs, player); if (pdom.find(".scout_report").length > 0) { vanGmMz.getScoutReport(pid, pdom); } } else if (pdom.find(".training_graphs").length > 0 && imgs.length > 0) { if (pdom.find(".scout_report").length > 0) { vanGmMz.getScoutReport(pid, pdom); } else if(pdom.find(".scout_report_stars")) { vanGmMz.getScoutReport(pid, pdom); } vanGmMz.getTrainingGraphs(pid, pdom, GraphsType); } else if (pdom.find(".scout_report").length > 0) { if (player) { this.setPlayerImgs(imgs, player); } else { for (let j = 0; j < imgs.length; j++) { vanGmMz.setSrc(false, imgs[j], parseInt(imgs[j].src.match(vanGmMzModel.mzreg.img_val)[1]), ""); } } vanGmMz.getScoutReport(pid, pdom, GraphsType == 2); } } return false; } , setPlayerImgs: function (imgs, player) { if (isNaN(parseInt(player.skills.stamina))) { for (let j = 0; j < imgs.length; j++) { vanGmMz.setSrc(false, imgs[j], parseInt(imgs[j].src.match(vanGmMzModel.mzreg.img_val)[1]), ""); } } else { if (vanGmMz.now_sport == "soccer") { vanGmMz.setSrc(false, imgs[0], player.skills.speed, player.maxed.speed); vanGmMz.setSrc(false, imgs[1], player.skills.stamina, player.maxed.stamina); vanGmMz.setSrc(false, imgs[2], player.skills.gameintelligence, player.maxed.gameintelligence); vanGmMz.setSrc(false, imgs[3], player.skills.passing, player.maxed.passing); vanGmMz.setSrc(false, imgs[4], player.skills.shooting, player.maxed.shooting); vanGmMz.setSrc(false, imgs[5], player.skills.heading, player.maxed.heading); vanGmMz.setSrc(false, imgs[6], player.skills.goalkeeping, player.maxed.goalkeeping); vanGmMz.setSrc(false, imgs[7], player.skills.technique, player.maxed.technique); vanGmMz.setSrc(false, imgs[8], player.skills.tackling, player.maxed.tackling); vanGmMz.setSrc(false, imgs[9], player.skills.highpassing, player.maxed.highpassing); vanGmMz.setSrc(false, imgs[10], player.skills.situations, player.maxed.situations); } else { vanGmMz.setSrc(false, imgs[1], player.skills.gameintelligence, player.maxed.gameintelligence); vanGmMz.setSrc(false, imgs[2], player.skills.power, player.maxed.power); vanGmMz.setSrc(false, imgs[3], player.skills.skating, player.maxed.skating); vanGmMz.setSrc(false, imgs[4], player.skills.passing, player.maxed.passing); vanGmMz.setSrc(false, imgs[5], player.skills.quickness, player.maxed.quickness); vanGmMz.setSrc(false, imgs[6], player.skills.shooting, player.maxed.shooting); vanGmMz.setSrc(false, imgs[7], player.skills.goalkeeping, player.maxed.goalkeeping); vanGmMz.setSrc(false, imgs[8], player.skills.puckcontrol, player.maxed.puckcontrol); vanGmMz.setSrc(false, imgs[9], player.skills.checking, player.maxed.checking); vanGmMz.setSrc(false, imgs[10], player.skills.stamina, player.maxed.stamina); } } }, drawPlayerByTrainingGraphs: function (pid, data, pdom) { let imgs = pdom.find("div.player-skills").find("img.ignore-fluid"); let old = false; if (imgs.length == 0) { imgs = pdom.find("img.skill"); old = true; } var series = undefined; eval(data); if (series == undefined) { return false; } let maxeds, skillBallDays, allSkillTraining_tmp; if (vanGmMz.now_sport == "soccer") { maxeds = ["green", "green", "green", "green", "green", "green", "green", "green", "green", "green", "green"]; skillBallDays = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; allSkillTraining_tmp = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; } else { imgs.splice(0, 1); maxeds = ["green", "green", "green", "green", "green", "green", "green", "green", "green", "green"]; skillBallDays = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; allSkillTraining_tmp = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; } let camp = new vanGmMzModel.mzcamp(); let NowSeasonInfo = vanGmMz.D_GetNowSeasonInfo(xPlotLines); for (let i = 0; i < series.length; i++) { if ((series[i].type == "line" && series[i].color == "rgba(255,0,0,0.7)")) { if (series[i].data.length > 0) { let g = series[i].data[0]; let index = g.y - 1; if (index >= 0 && g.y <= 11) { maxeds[index] = "red"; } } } else if ((series[i].type == "line" && series[i].color == "rgba(255,88,0,0.6)")) { //训练营线 if (series[i].data && series[i].data.length == 2) { camp.add(series[i].data[0].x, series[i].data[1].x); } } else { for (let j = 0; j < series[i].data.length; j++) { let g = series[i].data[j]; if (g.y == "-3") { let itemcamp = camp.getItem(g.x); if (itemcamp) { itemcamp.name = g.name; } } let index = g.y - 1; if (index >= 0 && g.y <= 11) { if (g.name == "Maxed") { maxeds[index] = "red"; } if (g.marker && g.marker.symbol) { if (allSkillTraining_tmp[index] == 0) { allSkillTraining_tmp[index] = new Array(); allSkillTraining_tmp[index].push(new vanGmMzModel.playerTrainingBySkill()); } if (/training_camp/.test(g.marker.symbol)) { //训练营 判断是否是ytc if (camp.inYTC(g.x)) { imgs[index].isYtc = true; } } let playerTS; //掉球单独列 if (/bar_neg/.test(g.marker.symbol)) { if (allSkillTraining_tmp[index].neg == undefined) { allSkillTraining_tmp[index].neg = new Array(); } if (/_ball/.test(g.marker.symbol) || /_puck/.test(g.marker.symbol)) { if (skillBallDays[index] < g.x) { skillBallDays[index] = g.x; playerTS = new vanGmMzModel.playerTrainingBySkill(); playerTS.ballDay = g.x; allSkillTraining_tmp[index].neg.push(playerTS); } } else if (allSkillTraining_tmp[index].neg.length == 0) { playerTS = new vanGmMzModel.playerTrainingBySkill(); allSkillTraining_tmp[index].neg.push(playerTS); } else { playerTS = allSkillTraining_tmp[index].neg[allSkillTraining_tmp[index].neg.length - 1]; } } else { playerTS = allSkillTraining_tmp[index][allSkillTraining_tmp[index].length - 1]; if (/_ball/.test(g.marker.symbol) || /_puck/.test(g.marker.symbol)) { if (skillBallDays[index] < g.x) { skillBallDays[index] = g.x; playerTS.ballDay = g.x; let next_playerTS = new vanGmMzModel.playerTrainingBySkill(); next_playerTS.last = playerTS; allSkillTraining_tmp[index].push(next_playerTS); } } } let result = g.marker.symbol.match(vanGmMzModel.mzreg.trainingType); let type = ""; if (result && result.length > 0) { switch (result[1]) { case "physio": //理疗 type = "physio"; break; case "coach": //有教练 type = "coach"; break; case "training_camp": //训练营 type = "camp"; break; case "training_camp_ycc": //调整营 type = "ycc"; break; case "training_camp_itc": //强化营 type = "itc"; break; default: //未知 console.log(vanGmMz.now_language.training_unknown + ":" + result[1]); break; } } vanGmMz.D_FillTraining(type, playerTS, g, NowSeasonInfo); } } } } } let allSkillTraining = new Array(); for (let t1 = 0; t1 < allSkillTraining_tmp.length; t1++) { if (imgs[t1].nowSkill == undefined) { imgs[t1].nowSkill = parseInt(imgs[t1].src.match(vanGmMzModel.mzreg.img_val)[1]); } let tmp = {}; let negCount = 0; if (allSkillTraining_tmp[t1].neg) { negCount = allSkillTraining_tmp[t1].neg.length; tmp.neg = {}; for (let t3 = 0; t3 < allSkillTraining_tmp[t1].neg.length; t3++) { let tmp3 = allSkillTraining_tmp[t1].neg[t3]; if (tmp3.stat.getSum() != 0) { tmp3.skill = imgs[t1].nowSkill + negCount - t3; tmp.neg[tmp3.skill] = tmp3; } } } for (let t2 = 0; t2 < allSkillTraining_tmp[t1].length; t2++) { let tmp2 = allSkillTraining_tmp[t1][t2]; if (tmp2.stat.getSum() != 0) { tmp2.skill = imgs[t1].nowSkill + negCount + 1 - allSkillTraining_tmp[t1].length + t2; tmp[tmp2.skill] = tmp2; } //test vanGmMz.vanlog(pid, tmp2, t1, t2); } allSkillTraining[t1] = tmp; } vanGmMz.trainingInfo[pid] = allSkillTraining; for (let k = 0; k < maxeds.length; k++) { vanGmMz.setSrc(pdom.find(".player_share_skills").length == 0, imgs[k], imgs[k].nowSkill, maxeds[k], skillBallDays[k], pid, k); } vanGmMz.D_NowSeasonText(pid, NowSeasonInfo, pdom); series = undefined; plotBands = undefined; xPlotLines = undefined; return true; } , vanlog: function (pid, pt, t1, t2) { }, fillTrainingLevel: function (type, reg, playerTS, url, isneg) { let result = url.match(reg); if (result && result.length > 0) { let stat = playerTS.stat; playerTS.lt = parseInt(result[1]); if (isneg) { stat.add(type, "t" + result[1]); } else { stat.add("all", "t" + result[1]); stat.add(type, "t" + result[1]); } } } , getScoutReport: function (pid, pdom, showMB, nochecka) { let url = "/ajax.php?p=players&sub=scout_report&pid=" + pid + "&sport=" + vanGmMz.now_sport; let cache_mode = 1; if (pdom.find("#discard_youth_button").length) { url = "/ajax.php?p=players&sub=scout_report&pid=null&sport=" + vanGmMz.now_sport; cache_mode = 0; } vple.ajax( url, function (data) { if (!nochecka) { if (pdom.find("a[href$='" + pid + "']").length == 0 && pdom.find("#player_id_" + pid).length == 0) { return; } } let srdom = $($.parseHTML(data)); let remark = srdom.find("span.blurred span").text(); let srdivs = srdom.find("dd div.flex-grow-1"); if (srdivs.length == 3) { let HS = srdivs.eq(0).find(".lit").length; let LS = srdivs.eq(1).find(".lit").length; let SS = srdivs.eq(2).find(".lit").length; //Trzxyvopaxis let Hspan = srdivs.eq(0).find(".blurred span"); let HArr = new Array(); for (let i = 0; i < Hspan.length; i++) { if (Hspan.eq(i).text() != "Trzxyvopaxis") { HArr.push(Hspan.eq(i).text()); } } let Lspan = srdivs.eq(1).find(".blurred span"); let LArr = new Array(); for (let i = 0; i < Lspan.length; i++) { if (Lspan.eq(i).text() != "Trzxyvopaxis") { LArr.push(Lspan.eq(i).text()); } } let HPids = [], LPids = []; let skillnames = pdom.find("div.player-skills").find("span.responsive-hide.responsive-container"); if (skillnames.length == 0) { skillnames = pdom.find("td > span.clippable"); } let onMarket = false; if(skillnames.length == 0){ skillnames = pdom.find("td > span.skill_name") onMarket = true; } for (let i = 0; i < skillnames.length; i++) { let skillText = onMarket ? skillnames.find("span:first").eq(i).text() : skillnames.eq(i).text() if (HArr.indexOf(skillText) >= 0) { skillnames.eq(i).parent().addClass("gm_scout_h"); skillnames.eq(i).parent().addClass("gm_s" + HS); HPids.push(i + 1); } else if (LArr.indexOf(skillText) >= 0) { skillnames.eq(i).parent().removeClass("gm_scout_h"); skillnames.eq(i).parent().addClass("gm_s" + LS); LPids.push(i + 1); } //else { // skillnames.eq(i).parent().removeClass("gm_scout_h"); // skillnames.eq(i).parent().removeClass("gm_s*"); //} } $("#GM_scout_" + pid).remove(); let nsavgstat = "<span id='GM_scout_" + pid + "'>[H" + HS + " " + HArr[0] + "," + HArr[1] + "] [L" + LS + " " + LArr[0] + "," + LArr[1] + "] S" + SS; nsavgstat += "</span>"; nsavgstat = $(nsavgstat)[0]; nsavgstat.addEventListener('click', function () { let strSus = remark; if (vanGmMz.IsLoser(HS, LS, LPids[0], LPids[1])) { strSus += "<br/><br/>" + vanGmMz.now_language.sug_Loser; } let plans = vanGmMz.getTrainPlans(HPids[0], HPids[1], LPids[0], LPids[1]); for (let j = 0; j < plans.length; j++) { let str = ""; let pri = vanGmMz.getTrainPRI(plans[j].loc, HS, HPids[0], HPids[1], LS, LPids[0], LPids[1]); for (let i = 0; i < pri.Order.length; i++) { if (str != "") { str += ">"; } str += vanGmMz.now_language["attrName" + pri.Order[i]]; } strSus += "<br/><br/>" + vanGmMz.now_language["sug_T" + plans[j].type] + vanGmMz.now_language["Pos" + pri.Sloc.CampKey] + "<br/><br/>" + vanGmMz.now_language.sug_PRI + str; } vanGmMz.D_ShowScoutText(strSus, pid, pdom, HS, HPids, LS, LPids); return false; }); pdom.find("a.subheader").after(nsavgstat); vanGmMz.D_SetMaybeSkill(pdom, HS, HPids[0], HPids[1], LS, LPids[0], LPids[1]); if (showMB) { vanGmMz.D_ShowMaybeSkill(pdom, HS, HPids[0], HPids[1], LS, LPids[0], LPids[1]); } } else { return true; } }, cache_mode); } , checkScoutLoc: function (lst, key, LP1, LP2, slocs) { if (lst[key] != undefined) { let sloc = lst[key]; if (vanGmMz.getProp(LP1, sloc) == 0 || vanGmMz.getProp(LP2, sloc) == 0) { slocs.push({ type: 1, loc: sloc }); } else { slocs.push({ type: 0, loc: sloc }); } slocs.keys[key] = true; } } , getProp: function (id, loc) { if (loc.Prop[id] != undefined) { return loc.Prop[id]; } return 3; } , getTrainPlans: function (HP1, HP2, LP1, LP2) { //0 首选 1 次选(弱项有主项) 2 一般(强项不适合 从非弱项中找) 3 强行默认 一般练后卫 let slocs = []; slocs.keys = {}; //按高星挑选合适训练计划 vanGmMz.checkScoutLoc(vanGmMzModel.scoutLocList, HP1 + "," + HP2, LP1, LP2, slocs); vanGmMz.checkScoutLoc(vanGmMzModel.scoutLocList, HP2 + "," + HP1, LP1, LP2, slocs); vanGmMz.checkScoutLoc(vanGmMzModel.scoutLocList, HP1, LP1, LP2, slocs); vanGmMz.checkScoutLoc(vanGmMzModel.scoutLocList, HP2, LP1, LP2, slocs); for (let i = 0; i < vanGmMzModel.OKeys.length; i++) { let key = vanGmMzModel.OKeys[i]; if (!slocs.keys[key]) { let loc = vanGmMzModel.scoutLocList[key]; if (vanGmMz.getProp(LP1, loc) != 1 && vanGmMz.getProp(LP2, loc) != 1) { slocs.push({ type: 2, loc: loc }); slocs.keys[key] = true; } } } if (slocs.length == 0) { slocs.push({ type: 3, loc: vanGmMzModel.scoutLocList[vanGmMzModel.OKeys[0]] }); } return slocs; } , IsLoser: function (HStar, LStar, LP1, LP2) { if (HStar <= 2) { return true; } //弱1带速耐 if (LStar == 1 && HStar < 4) { if (LP1 == 1 || LP2 == 1 || LP1 == 2 || LP2 == 2) { return true; } } return false; } , getTrainPRI: function (sloc, HStar, HP1, HP2, LStar, LP1, LP2) { //获取训练顺序 let lstBase = []; let lstMain = []; let lstSub = []; for (let i = 0; i < sloc.Order.length; i++) { let ID = sloc.Order[i]; let t = vanGmMz.getProp(ID, sloc); if (t == 0 || t == 1) { //强3+星 基础和主项训练完再训练 if (HStar >= 3) { if (HP1 == ID || HP2 == ID) { lstSub.push(ID); } } //弱2星带基础和主项 最优先训练 if (LStar <= 2) { if (LP1 == ID || LP2 == ID) { if (t == 0) { lstBase.unshift(ID); } else { lstMain.push(ID); } } } //高低星都没有基础和主项 最优先训练 if (HP1 != ID && HP2 != ID && LP1 != ID && LP2 != ID) { //意降低优先级 if (ID == 3) { lstSub.push(ID); } else if (t == 0) { lstBase.push(ID); } else { lstMain.push(ID); } } } else if (t == 2) { lstSub.push(ID); } } let pri = 29; let ditPRI = {}; let lstOrder = lstBase.concat(lstMain).concat(lstSub); while (lstBase.length > 0) { ditPRI[lstBase.shift()] = pri; pri -= 1; } pri = 19; while (lstMain.length > 0) { ditPRI[lstMain.shift()] = pri; pri -= 1; } pri = 9; while (lstSub.length > 0) { ditPRI[lstSub.shift()] = pri; pri -= 1; } let ret = { "Sloc": sloc, "TrainPRI": ditPRI, "Order": lstOrder }; return ret; } , getTrainingGraphs: function (pid, pdom, GraphsType) { vple.ajax( "/ajax.php?p=trainingGraph&sub=getJsonTrainingHistory&sport=" + vanGmMz.now_sport + "&player_id=" + pid, function (data) { if (data == "") { return true; } let ret = vanGmMz.drawPlayerByTrainingGraphs(pid, data, pdom); if (GraphsType == 2 && pdom.find(".scout_report").length > 0) { vanGmMz.getScoutReport(pid, pdom, true); } return !ret; }); }, getTrainingGraphsBySkill_id: function (pid, skill_id, callback) { if (vanGmMz.now_sport == "soccer") { skill_id += 2; } else { if (skill_id == 0) { skill_id == 11; } else { skill_id += 1; } } vple.ajax( "/ajax.php?p=trainingGraph&sub=getJsonTrainingHistory&sport=" + vanGmMz.now_sport + "&player_id=" + pid + "&skill_id=" + skill_id, function (data) { return !callback(data); }); }, showPop: function (parent) { $("body").append("<div id=\"informationBubble\" class=\"shadow\"></div>"); let bubble = $("#informationBubble"); bubble.css("width", "200px"); let tmpArr = parent.attr('id').split("_"); let playTS = vanGmMz.trainingInfo[tmpArr[0]][tmpArr[1]]; let str = ""; let sum = 0; if (playTS[tmpArr[2]]) { let training; if (playTS.neg) { training = playTS.neg[parseInt(tmpArr[2]) + 1].stat; } else { training = playTS[tmpArr[2]].stat; } str = vanGmMz.now_language.training_avg + " " + training.getAvg() + "%" + training.getTnText(vanGmMz.now_language) + "<br/><br/>" + vanGmMz.now_language.training_ball_day + ":<br/>"; if (training.camp) { str += training.getDayByAvg(training.camp.getAvg()) + "(" + vanGmMz.now_language.training_camp + ") "; } if (training.coach) { str += training.getDayByAvg(training.coach.getAvg()) + "(" + vanGmMz.now_language.training_coach + ") "; } if (training.pos) { str += training.getDayByAvg(training.pos.getAvg()) + "(" + vanGmMz.now_language.training_pos + ")"; } str += "<br/><br/>"; sum = training.getSum(); } let flag = false; str += vanGmMz.now_language.training_part + ":"; for (let i = 0; i < 10; i++) { if (playTS[i]) { flag = true; str += "<br/>" + i + "-" + (i + 1) + " " + vanGmMz.now_language.training_total + ":" + playTS[i].stat.getSum(true) + "%" + " " + vanGmMz.now_language.training_avg + ":" + playTS[i].stat.getAvg() + "%<br/>" + playTS[i].stat.getTnText(vanGmMz.now_language); } } if (playTS.neg) { for (let i = 10; i > 0; i--) { if (playTS.neg[i]) { flag = true; str += "<br/>" + i + "-" + (i - 1) + " " + vanGmMz.now_language.training_total + ":" + playTS.neg[i].stat.getSum(true) + "%" + " " + vanGmMz.now_language.training_avg + ":" + playTS.neg[i].stat.getAvg() + "%<br/>" + playTS.neg[i].stat.getTnText(vanGmMz.now_language); } } } let content = "<div class='clearfix'><h3 style='margin: 0; padding: 0'>" + vanGmMz.now_language.training_now + ":" + sum + "%</h3><div class='skill_exact big'><div class='skill_exact_wrapper clearfix'><div class='skill_exact_bar' style='width: " + sum * 2 + "px;'></div></div></div><p>" + (flag ? str : "") + "</p></div>"; bubble.html(content); mz.stickToParent(bubble, parent); bubble.slideDown("fast"); $(parent).bind("mouseleave", function () { bubble.remove(); $(parent).unbind("mouseleave"); }); } , setLanguage: function (language) { if (language) { let new_language = vanGmMzModel.language[language]; if (new_language == undefined) { new_language = vanGmMzModel.language.en; } if (vanGmMz.now_language != new_language) { GM_setValue("mylanguage", language); } vanGmMz.now_language = new_language; } else { if (vanGmMz.now_language == vanGmMzModel.language.en) { vanGmMz.now_language = vanGmMzModel.language.cn; GM_setValue("mylanguage", "cn"); } else { vanGmMz.now_language = vanGmMzModel.language.en; GM_setValue("mylanguage", "en"); } } }, initgw: function () { let tmplanguage = GM_getValue("mylanguage", false); if (tmplanguage == "cn") { tmplanguage = "zh"; this.setLanguage(tmplanguage); } if (vanGmMzModel.language[tmplanguage] == undefined) { let lang = $("meta[name='language']"); if (lang.length > 0) { this.setLanguage($("meta[name='language']")[0].content); } } else { this.now_language = vanGmMzModel.language[tmplanguage]; } let tmpTacConf = GM_getValue("TacConf", ""); if (tmpTacConf && tmpTacConf.trim() != "") { vanGmMz.tacCof = JSON.parse(tmpTacConf); } else { vanGmMz.tacCof = vpleModel.tacCof; } let css = document.createElement('style'); css.type = 'text/css'; css.innerHTML = ".gw_run_div{position:fixed;bottom:20%;right:1px;border:1px solid gray;padding:3px;width:12px;font-size:12px;border-radius: 3px;text-shadow: 1px 1px 3px #676767;background-color: #000000;color: #FFFFFF;cursor: default;}.gw_run{cursor:pointer;}.gw_div_left{float:left;position:fixed;left:0px;top:120px;height:528px;overflow-y:auto;text-align:left;}.gw_div_right{float:right;position:fixed;right:0px;top:120px;height:528px;overflow-y:auto;text-align:left;}.shupai{writing-mode:tb-rl;-webkit-writing-mode:vertical-rl;}.gm_ytc{font-style:italic;text-decoration:underline;}.gm_scout_h{font-weight: bold;}.gm_s1{color:red;}.gm_s2{color:darkgoldenrod;}.gm_s3{color:blue;}.gm_s4{color:fuchsia;}"; document.getElementsByTagName('head')[0].appendChild(css); window.__mzcMega.registerAction( "legacy-color", this.now_language.ManualColor, this.now_language.ManualColorTitle, function () { vanGmMz.gw_start(1); }, "gw_run" ); window.__mzcMega.registerAction( "legacy-settings", this.now_language.Setting, this.now_language.SettingTitle, function () { vanGmMz.OpenSetting(); }, "gw_run2" ); document.onkeydown = function () { if (event.altKey) { if (window.event.keyCode == 65) { //alt + A vanGmMz.gw_start(1); } else if (window.event.keyCode == 83) { //alt + S vanGmMz.gw_start(2); } else if (window.event.keyCode == 68) { //alt + D vanGmMz.Advanced2D(); } } }; $("body").on("mouseenter", ".skill_exact_van", function () { vanGmMz.showPop($(this)); }); let mth = location.href.match(vanGmMzModel.mzreg.matchResult); if (mth && mth.length > 0) { var mid = mth[1]; let amatch = $("a.matchIcon.large.shadow"); if (amatch.length > 0) { amatch.eq(0).before('<a id="gw_match_result" class="matchIcon large shadow" href="javascript:void(0);" rel="nofollow" title="Watch match result"><i>R</i><span> </span></a>'); $('#gw_match_result')[0].addEventListener('click', function () { vanGmMz.ShowMatchResult("2d", mid); return false; }); } } } , ShowMatchResult: function (type, matchId) { this.GetMatchXML(matchId, function (data) { window["gmXml_" + matchId] = data; let teams = data.documentElement.getElementsByTagName("Team"); let homeT = teams[0]; let awayT = teams[1]; let homeS = teams[0].getElementsByTagName("Statistics")[0]; let awayS = teams[1].getElementsByTagName("Statistics")[0]; let imgs = $("div.scoreboard.shadow img"); let mth = imgs[0].src.match(vanGmMzModel.mzreg.nocache); let nocacheUrl = "nocache-715"; if (mth && mth.length > 0) { nocacheUrl = mth[0]; } let g1 = homeS.getAttribute("goals"); if (g1 >= 10) { imgs[0].src = nocacheUrl + "/img/score/" + parseInt(g1 / 10) + ".gif"; } else { imgs[0].src = nocacheUrl + "/img/score/null.gif"; } imgs[1].src = nocacheUrl + "/img/score/" + g1 % 10 + ".gif"; let g2 = awayS.getAttribute("goals"); if (g2 >= 10) { imgs[3].src = nocacheUrl + "/img/score/" + parseInt(g2 / 10) + ".gif"; imgs[4].src = nocacheUrl + "/img/score/" + g2 % 10 + ".gif"; } else { imgs[3].src = nocacheUrl + "/img/score/" + g2 % 10 + ".gif"; imgs[4].src = nocacheUrl + "/img/score/null.gif"; } let td = $("div#match-tactic-facts-wrapper div div table.hitlist.statsLite tbody tr td"); td.eq(1).html(g1); td.eq(2).html(g2); if (vanGmMz.now_sport == "soccer") { td.eq(4).html(homeS.getAttribute("injuries")); td.eq(5).html(awayS.getAttribute("injuries")); td.eq(7).html(homeS.getAttribute("yellowCards")); td.eq(8).html(awayS.getAttribute("yellowCards")); td.eq(10).html(homeS.getAttribute("redCards")); td.eq(11).html(awayS.getAttribute("redCards")); td.eq(13).html(homeS.getAttribute("freekicks")); td.eq(14).html(awayS.getAttribute("freekicks")); td.eq(16).html(homeS.getAttribute("penaltyshots")); td.eq(17).html(awayS.getAttribute("penaltyshots")); td.eq(19).html(homeS.getAttribute("corners")); td.eq(20).html(awayS.getAttribute("corners")); td.eq(22).html(homeS.getAttribute("shotsOnGoal")); td.eq(23).html(awayS.getAttribute("shotsOnGoal")); td.eq(25).html(homeS.getAttribute("possession") + "%"); td.eq(26).html(awayS.getAttribute("possession") + "%"); $(".gm_timeline").remove(); let events = data.documentElement.getElementsByTagName("Events"); if (events.length) { let tacs = events[0].getElementsByTagName("Tactic"); let trclass = false; let timeline = $("table.timeline.hitlist.marker"); if (timeline.length) { for (let i = tacs.length - 1; i >= 0; i--) { if (tacs[i].getAttribute("teamId") == homeT.getAttribute("id")) { timeline.find("tr:first").before("<tr class='gm_timeline " + (trclass ? "even" : "odd") + "'><td align='right' width='50%'><span style='white-space: nowrap'><strong>" + tacs[i].getAttribute("type") + "->" + tacs[i].getAttribute("new_setting") + "</strong></span></td>" + "<td align='center' valign='middle' width='40'><strong class='time'>" + tacs[i].getAttribute("time").split(":")[0] + "'</strong></td>" + "<td align='left' width='50%'> </td></tr>"); } else { timeline.find("tr:first").before("<tr class='gm_timeline " + (trclass ? "even" : "odd") + "'><td align='right' width='50%'> </td>" + "<td align='center' valign='middle' width='40'><strong class='time'>" + tacs[i].getAttribute("time").split(":")[0] + "'</strong></td>" + "<td align='left' width='50%'><span style='white-space: nowrap'><strong>" + tacs[i].getAttribute("type") + "->" + tacs[i].getAttribute("new_setting") + "</strong></span></td></tr>"); } trclass = !trclass; } timeline.find("tr:first").before("<tr class='gm_timeline " + (trclass ? "even" : "odd") + "'><td align='right' width='50%'><span style='white-space: nowrap'><strong>" + homeT.getAttribute("tactic") + "</strong><strong>" + homeT.getAttribute("playstyle") + "</strong><strong>" + homeT.getAttribute("aggression") + "</strong><p><a href='javascript:void(0);' id='gm_copy_r1'>" + vanGmMz.now_language.Copyxml1 + "</a></p></span></td><td align='center' valign='middle' width='40'><strong class='time'>0'</strong></td><td align='left' width='50%'><span style='white-space: nowrap'><strong>" + awayT.getAttribute("tactic") + "</strong><strong>" + awayT.getAttribute("playstyle") + "</strong><strong>" + awayT.getAttribute("aggression") + "</strong><p><a href='javascript:void(0);' id='gm_copy_r2'>" + vanGmMz.now_language.Copyxml2 + "</a></p></span></td></tr>"); $('#gm_copy_r1')[0].addEventListener('click', function () { vanGmMz.CopyXML(matchId, true); return false; }); $('#gm_copy_r2')[0].addEventListener('click', function () { vanGmMz.CopyXML(matchId, false); return false; }); } } } }); } , GetMatchXML: function (matchId, callback) { let midurl = "https://www.managerzone.com/matchviewer/getMatchFiles.php?type=stats&mid=" + matchId + "&sport=" + vanGmMz.now_sport; var _overlay = this; vple.getLocValue(midurl, function (flag, tdata) { if (flag) { let data = $.parseXML(tdata); return callback(data); } else { _overlay.prepareMatch = function () { $.getJSON(mz.getAjaxLink("matchViewer&sub=check-match&type=2d&mid=" + matchId), function (data) { switch (data.response) { case "ok": vple.ajax( midurl, function (tdata) { let data = $.parseXML(tdata); return callback(data); }, 1, false); break; case "queued": _overlay.tryCounter++; if (_overlay.tryCounter > 5) { return false; } setTimeout(function () { _overlay.prepareMatch(); }, 3000); break; case "walkover": case "blocked": case "error": return false; } }); }; _overlay.tryCounter = 1; _overlay.prepareMatch(); } }, 1, false); } , //GraphsType 0 自动模式 1 强制训练图 2 星级球员显示最大值 gw_start: function (GraphsType) { $(".player_id_span").unbind("click"); $(".player_id_span").bind("click", function () { vanGmMz.showScore(this.innerText, $(this).parents(".playerContainer")); }); if ($("#players_container").width() < 660) { if (vanGmMzModel.mzreg.shortlist_url.test(location.href)) { $(".col_2_of_3").width("660"); } $("#players_container").width("660"); } if ($(".player_share_skills").length > 0) { if (GraphsType == 0) { vanGmMz.getMax(function () { vanGmMz.showMax(0); }); } else { vanGmMz.showMax(GraphsType); } } else if ($(".playerContainer").find(".training_graphs").length > 0) { vanGmMz.showMax(GraphsType); } else if ($(".playerContainer").find(".scout_report").length > 0) { if (GraphsType == 0) { vanGmMz.getMax(function () { vanGmMz.showMax(GraphsType); }); } else { vanGmMz.showMax(GraphsType); } } } , OpenSetting: function () { let lang = GM_getValue("mylanguage", "en"); let xml_mode = GM_getValue("xml_mode", 0); let autoRun = GM_getValue("autoRun1", 1); let tmphtml = '<div><b>' + vanGmMz.now_language.Language + ':</b></div><div><select id="gm_language">'; for (let l in vanGmMzModel.language) { tmphtml += '<option value="' + l + '"' + (lang == l ? ' selected="selected" ' : '') + '>' + vanGmMzModel.language[l].Name + '</option>'; } tmphtml += '</select>\ </div>\ \ <div><b>'+ vanGmMz.now_language.AutoRun + ':</b></div>\ <div><select id="gm_autorun">\ <option value="0"'+ (autoRun == 0 ? ' selected="selected" ' : '') + '>' + vanGmMz.now_language.AutoRun0 + '</option>\ <option value="1"'+ (autoRun == 1 ? ' selected="selected" ' : '') + '>' + vanGmMz.now_language.AutoRun1 + '</option>\ </select>\ </div>\ \ <div><b>'+ vanGmMz.now_language.XmlMode + ':</b></div>\ <div><select id="gm_xml_mode">\ <option value="0"'+ (xml_mode == 0 ? ' selected="selected" ' : '') + '>' + vanGmMz.now_language.XmlMode1 + '</option>\ <option value="1"'+ (xml_mode == 1 ? ' selected="selected" ' : '') + '>' + vanGmMz.now_language.XmlMode2 + '</option>\ </select>\ </div>\ <div><b>'+ vanGmMz.now_language.TacConf + ':</b></div>\ <div><textarea style="width: 380px;height:200px;" id="txtTacConf" spellcheck="false" autocapitalize="off" autocomplete="off" autocorrect="off"></textarea>\ </div>\ <div>\ <a href="javascript:void(0);" class="mzbtn buttondiv button_account" id="gm_setting_save">\ <span class="buttonClassMiddle"><span style="white-space: nowrap">'+ vanGmMz.now_language.Save + '</span></span><span class="buttonClassRight"> </span>\ </a><hr />\ <a href="javascript:void(0);" class="mzbtn buttondiv button_account" id="gm_setting_players_html">\ <span class="buttonClassMiddle"><span style="white-space: nowrap">'+ vanGmMz.now_language.GetPlayersHtml + '</span></span><span class="buttonClassRight"> </span>\ </a>\ <a href="javascript:void(0);" class="mzbtn buttondiv button_account" id="gm_setting_players_html2">\ <span class="buttonClassMiddle"><span style="white-space: nowrap">'+ vanGmMz.now_language.GetPlayersHtml2 + '</span></span><span class="buttonClassRight"> </span>\ </a><hr />\ <a href="javascript:void(0);" class="mzbtn buttondiv button_account" id="gm_setting_clear">\ <span class="buttonClassMiddle"><span style="white-space: nowrap">'+ vanGmMz.now_language.Clear + '</span></span><span class="buttonClassRight"> </span>\ </a>\ <hr>'+ vanGmMz.now_language.Pay + '<hr>PayPal:<div><a href="https://www.paypal.me/vanjoge">paypal.me/vanjoge</a> or <b>[email protected]</b></div>\ <hr>\ 支付宝扫码: \ </div>\ '; window.__mzcMega.showInlineSettings("legacy-settings", vanGmMz.now_language.Setting, tmphtml); this.D_OpenSetting(); $("#txtTacConf").val(GM_getValue("TacConf", "")); $("#gm_setting_save")[0].addEventListener('click', function () { vanGmMz.setLanguage($("#gm_language").val()); GM_setValue("xml_mode", $("#gm_xml_mode").val()); GM_setValue("autoRun1", $("#gm_autorun").val()); let tmpTacConf = $("#txtTacConf").val(); GM_setValue("TacConf", tmpTacConf); if (tmpTacConf && tmpTacConf.trim() != "") { vanGmMz.tacCof = JSON.parse(tmpTacConf); } else { vanGmMz.tacCof = vpleModel.tacCof; } $('#gw_run')[0].title = vanGmMz.now_language.ManualColorTitle; $('#gw_run').html("<b>" + vanGmMz.now_language.ManualColor + "</b>"); $('#gw_run2')[0].title = vanGmMz.now_language.SettingTitle; $('#gw_run2').html("<b>" + vanGmMz.now_language.Setting + "</b>"); $('#gw_jijing').html(vanGmMz.now_language.Jijing); $('#gw_dongzuo').html(vanGmMz.now_language.dongzuo); $('#gw_copyxml1').html(vanGmMz.now_language.Copyxml1); $('#gw_copyxml2').html(vanGmMz.now_language.Copyxml2); $('#gw_test').html(vanGmMz.now_language.Test); powerboxCloseAll(); }); $("#gm_setting_clear")[0].addEventListener('click', function () { vple.cacheItem.clearAll(); }); $("#gm_setting_players_html")[0].addEventListener('click', function () { vanGmMz.GetPlayerHtml(true); }); $("#gm_setting_players_html2")[0].addEventListener('click', function () { vanGmMz.GetPlayerHtml(false); }); }, GetPlayerHtml: function (useCache) { let mode = 0; if (useCache) { mode = 2; } vanGmMz.GetPlayerHtmlByEn(mode, false, function (data) { GM_setClipboard(data); alert(vanGmMz.now_language.CopyXml); }); }, GetPlayerHtmlByEn: function (mode, Cjson, callback) { var plang = $("meta[name=language]").attr("content"); if (plang != "en") { $.get("/ajax.php?p=settings&sub=lang&sport=" + vanGmMz.now_sport + "&lang=en", function (data, status) { }); } vple.ajax( "/?p=players&sport=" + vanGmMz.now_sport, function (data) { callback(data); }, mode, Cjson); if (plang != "en") { $.get("/ajax.php?p=settings&sub=lang&sport=" + vanGmMz.now_sport + "&lang=" + plang, function (data, status) { }); } } , mEvent: undefined, mStaticEventHome: undefined, mStaticEventAway: undefined, out_of_play: undefined, dit_bypid: {}, dit_player: {}, m_koFrame: 0, m_htFrame: 0, m_ko2Frame: 0, m_ht2Frame: 0, m_ko3Frame: 0, m_ht3Frame: 0, m_ko4Frame: 0, m_ht4Frame: 0, Advanced2D: function () { if (vanGmMz.OK_2D) { if ($("#canvas").length > 0) { let home = MyGame.prototype.mzlive.m_match.getHomeTeam(); let away = MyGame.prototype.mzlive.m_match.getAwayTeam(); if (home != null && away != null) { window["gmXml_" + MyGame.prototype.mzlive.m_match.m_matchId] = $.parseXML(window.matchLoader.matchXml.xmlText); let nl = window.matchLoader.matchXml.documentElement.evaluate('Periods/*'); let p; while (p = nl.iterateNext()) { if (p.getAttribute('name') == 'half1') { vanGmMz.m_koFrame = MyGame.prototype.mzlive.m_match.timeToFrame(p.getAttribute('start')); vanGmMz.m_htFrame = MyGame.prototype.mzlive.m_match.timeToFrame(p.getAttribute('end')); } else if (p.getAttribute('name') == 'half2') { vanGmMz.m_ko2Frame = MyGame.prototype.mzlive.m_match.timeToFrame(p.getAttribute('start')); vanGmMz.m_ht2Frame = MyGame.prototype.mzlive.m_match.timeToFrame(p.getAttribute('end')); } else if (p.getAttribute('name') == 'extra1') { vanGmMz.m_ko3Frame = MyGame.prototype.mzlive.m_match.timeToFrame(p.getAttribute('start')); vanGmMz.m_ht3Frame = MyGame.prototype.mzlive.m_match.timeToFrame(p.getAttribute('end')); } else if (p.getAttribute('name') == 'extra2') { vanGmMz.m_ko4Frame = MyGame.prototype.mzlive.m_match.timeToFrame(p.getAttribute('start')); vanGmMz.m_ht4Frame = MyGame.prototype.mzlive.m_match.timeToFrame(p.getAttribute('end')); } } let players = window.matchLoader.matchXml.documentElement.evaluate('Player'); let re1; vanGmMz.dit_bypid = {}; vanGmMz.dit_player = {}; while (re1 = players.iterateNext()) { let ttt = { internalId: re1.getAttribute('internalId'), name: re1.getAttribute('name'), shirtno: re1.getAttribute('shirtno'), teamId: re1.getAttribute('teamId'), origin: re1.getAttribute('origin') }; vanGmMz.dit_bypid[re1.getAttribute('id')] = ttt; if (ttt.teamId == home.m_teamId) { vanGmMz.dit_player[ttt.internalId] = home.getPlayerByPlayerId(ttt.internalId); } else { vanGmMz.dit_player[ttt.internalId] = away.getPlayerByPlayerId(ttt.internalId); } } let events = window.matchLoader.matchXml.documentElement.evaluate('Events/*'); let re, begin, end; let homeTc = new Array(); let awayTc = new Array(); vanGmMz.out_of_play = new vanGmMzModel.OutOfPlay(); vanGmMz.out_of_play.add(0, vanGmMz.m_koFrame); vanGmMz.out_of_play.add(vanGmMz.m_htFrame, vanGmMz.m_ko2Frame); if (vanGmMz.m_ko3Frame > 0) { vanGmMz.out_of_play.add(vanGmMz.m_ht2Frame, vanGmMz.m_ko3Frame); } if (vanGmMz.m_ko4Frame > 0) { vanGmMz.out_of_play.add(vanGmMz.m_ht3Frame, vanGmMz.m_ko4Frame); } while (re = events.iterateNext()) { begin = re.getAttribute('intervalendframe'); end = re.getAttribute('startframe'); if (begin != undefined && end != undefined) { vanGmMz.out_of_play.add(begin, end); } //战术变动 if (re.tagName == 'Tactic') { let t_teamid = re.getAttribute('teamId'); let t_time = re.getAttribute('time'); let t_type = re.getAttribute('type'); let t_new_setting = re.getAttribute('new_setting'); if (t_teamid == home.m_teamId) { homeTc.push({ tag: 'Tactic', m_frame: MyGame.prototype.mzlive.m_match.timeToFrame(t_time), type: t_type, new_setting: t_new_setting, m_team: home }); } else { awayTc.push({ tag: 'Tactic', m_frame: MyGame.prototype.mzlive.m_match.timeToFrame(t_time), type: t_type, new_setting: t_new_setting, m_team: away }); } } //换人 else if (re.tagName == 'Substitution') { let t_time = re.getAttribute('time'); let t_clock = re.getAttribute('clock'); let t_frame = re.getAttribute('frame'); let t_playerId = re.getAttribute('playerId'); let t_teamId = re.getAttribute('teamId'); let t_substitutedId = re.getAttribute('substitutedId'); let t_reason = re.getAttribute('reason'); let t_minute = re.getAttribute('minute'); let t_scorecondition = re.getAttribute('scorecondition'); let t_score = re.getAttribute('score'); let t_player, t_sub_player; let p5 = vanGmMz.dit_bypid[t_playerId]; if (p5) { t_player = vanGmMz.dit_player[p5.internalId]; } p5 = vanGmMz.dit_bypid[t_substitutedId]; if (p5) { t_sub_player = vanGmMz.dit_player[p5.internalId]; } if (t_teamId == home.m_teamId) { homeTc.push({ tag: 'Sub', m_frame: t_frame, clock: t_clock, player: t_player, sub_player: t_sub_player, reason: t_reason, minute: t_minute, scorecondition: t_scorecondition, score: t_score, m_team: home }); } else { awayTc.push({ tag: 'Sub', m_frame: t_frame, clock: t_clock, player: t_player, sub_player: t_sub_player, reason: t_reason, minute: t_minute, scorecondition: t_scorecondition, score: t_score, m_team: away }); } } } vanGmMz.out_of_play.Sort(); let lstEventHome = new vanGmMzModel.MatchEvent(); let lstEventAway = new vanGmMzModel.MatchEvent(); lstEventHome.setAllPlayerEvent(home, homeTc); lstEventAway.setAllPlayerEvent(away, awayTc); lstEventHome.Sort(); lstEventAway.Sort(); vanGmMz.mStaticEventHome = lstEventHome; vanGmMz.mStaticEventAway = lstEventAway; let lstEvent2 = new vpleModel.MatchEvent2(); lstEvent2.setData(MyGame.prototype.mzlive.m_match, vanGmMz); vanGmMz.mEvent = lstEvent2; if ($('.gw_div_left').length == 0) { $('#canvas').parent().append('<div class="gw_div_left"></div>'); $('#canvas').parent().append('<div class="gw_div_right"></div>'); $('#canvas').parent().append( '<div><b id="gw_jijing" class="gw_run" style="color: red;">' + vanGmMz.now_language.Jijing + '</b>' + ' <b id="gw_dongzuo" class="gw_run" style="color: red;">' + vanGmMz.now_language.dongzuo + '</b>' + ' <b id="gw_copyxml1" class="gw_run" style="color: red;">' + vanGmMz.now_language.Copyxml1 + '</b>' + ' <b id="gw_copyxml2" class="gw_run" style="color: red;">' + vanGmMz.now_language.Copyxml2 + '</b>' + ' <b id="gw_test" class="gw_run" style="color: red;">' + vanGmMz.now_language.Test + '</b>' + '</div>'); $('#gw_jijing')[0].addEventListener('click', function () { vanGmMz.ShowDiv(0); }); $('#gw_dongzuo')[0].addEventListener('click', function () { vanGmMz.ShowDiv(1); }); $('#gw_copyxml1')[0].addEventListener('click', function () { vanGmMz.CopyXML(MyGame.prototype.mzlive.m_match.m_matchId, true); }); $('#gw_copyxml2')[0].addEventListener('click', function () { vanGmMz.CopyXML(MyGame.prototype.mzlive.m_match.m_matchId, false); }); $('#gw_test')[0].addEventListener('click', function () { }); } else { $('.gw_div_left').empty(); $('.gw_div_right').empty(); } } } } } , ShowDiv: function (type) { $('.gw_div_left').empty(); $('.gw_div_right').empty(); if (type == 0) { let lstEventHome = vanGmMz.mStaticEventHome; let lstEventAway = vanGmMz.mStaticEventAway; let team = MyGame.prototype.mzlive.m_match.getHomeTeam(); $('.gw_div_left').append("<div><b>" + team.getTactics() + " " + team.getPlayStyle() + " " + team.getAggression() + "</b></div>"); for (let i = 0; i < lstEventHome.data.length; i++) { if (lstEventHome.data[i].tag == "Tactic") { $('.gw_div_left').append('<div><b id="gw_eventH' + i + '" class="gw_run">' + MyGame.prototype.mzlive.m_match.frameToMatchMinute(lstEventHome.data[i].m_frame) + "′ " + lstEventHome.data[i].type + " -> " + lstEventHome.data[i].new_setting + '</b></div>'); } else if (lstEventHome.data[i].tag == "Sub") { $('.gw_div_left').append('<div><b id="gw_eventH' + i + '" class="gw_run">' + MyGame.prototype.mzlive.m_match.frameToMatchMinute(lstEventHome.data[i].m_frame) + "′ " + lstEventHome.data[i].player.m_name + "(" + lstEventHome.data[i].player.m_shirtNo + ")↑ " + lstEventHome.data[i].sub_player.m_name + "(" + lstEventHome.data[i].sub_player.m_shirtNo + ")↓<br/>" + lstEventHome.data[i].reason + lstEventHome.data[i].minute + "′[" + lstEventHome.data[i].scorecondition + " " + lstEventHome.data[i].score + "]" + '</b></div>'); } else { $('.gw_div_left').append('<div><b id="gw_eventH' + i + '" class="gw_run">' + MyGame.prototype.mzlive.m_match.frameToMatchMinute(lstEventHome.data[i].m_frame) + "′ " + lstEventHome.data[i].m_owner.m_name + "(" + lstEventHome.data[i].m_owner.m_shirtNo + ") " + lstEventHome.data[i].m_description + '</b></div>'); } let dom = $('#gw_eventH' + i)[0]; dom.m_frame = lstEventHome.data[i].m_frame; dom.m_frame -= 45; if (dom.m_frame < 0) { dom.m_frame = 0; } dom.addEventListener('click', function () { MyGame.prototype.mzlive.m_match.setCurrentFrame(this.m_frame); }); } team = MyGame.prototype.mzlive.m_match.getAwayTeam(); $('.gw_div_right').append("<div><b>" + team.getTactics() + " " + team.getPlayStyle() + " " + team.getAggression() + "</b></div>"); for (let i = 0; i < lstEventAway.data.length; i++) { if (lstEventAway.data[i].tag == "Tactic") { $('.gw_div_right').append('<div><b id="gw_eventA' + i + '" class="gw_run">' + MyGame.prototype.mzlive.m_match.frameToMatchMinute(lstEventAway.data[i].m_frame) + "′ " + lstEventAway.data[i].type + " -> " + lstEventAway.data[i].new_setting + '</b></div>'); } else if (lstEventAway.data[i].tag == "Sub") { $('.gw_div_right').append('<div><b id="gw_eventA' + i + '" class="gw_run">' + MyGame.prototype.mzlive.m_match.frameToMatchMinute(lstEventAway.data[i].m_frame) + "′ " + lstEventAway.data[i].player.m_name + "(" + lstEventAway.data[i].player.m_shirtNo + ")↑ " + lstEventAway.data[i].sub_player.m_name + "(" + lstEventAway.data[i].sub_player.m_shirtNo + ")↓<br/>" + lstEventAway.data[i].reason + lstEventAway.data[i].minute + "′[" + lstEventAway.data[i].scorecondition + " " + lstEventAway.data[i].score + "]" + '</b></div>'); } else { $('.gw_div_right').append('<div><b id="gw_eventA' + i + '" class="gw_run">' + MyGame.prototype.mzlive.m_match.frameToMatchMinute(lstEventAway.data[i].m_frame) + "′ " + lstEventAway.data[i].m_owner.m_name + "(" + lstEventAway.data[i].m_owner.m_shirtNo + ") " + lstEventAway.data[i].m_description + '</b></div>'); } let dom = $('#gw_eventA' + i)[0]; dom.m_frame = lstEventAway.data[i].m_frame; dom.m_frame -= 45; if (dom.m_frame < 0) { dom.m_frame = 0; } dom.addEventListener('click', function () { MyGame.prototype.mzlive.m_match.setCurrentFrame(this.m_frame); }); } } else { for (let pid in vanGmMz.mEvent.dataByPlayer) { let divname; if (vanGmMz.mEvent.dataByPlayer[pid].isHome) { divname = '.gw_div_left'; } else { divname = '.gw_div_right'; } let frame_count = 0; frame_count = vanGmMz.mEvent.dataByPlayer[pid].FoolCount; $(divname).append('<div><b id="gw_player_' + pid + '" class="gw_run">' + vanGmMz.mEvent.dataByPlayer[pid].owner.m_name + "(" + vanGmMz.mEvent.dataByPlayer[pid].owner.m_shirtNo + ")" + "[" + frame_count + "]</b></div>"); let dom = $("#gw_player_" + pid)[0]; dom.pid = pid; dom.divname = divname; dom.addEventListener('click', function () { $(this.divname).empty(); $(divname).append('<div><b>' + vanGmMz.mEvent.dataByPlayer[pid].owner.m_name + "(" + vanGmMz.mEvent.dataByPlayer[pid].owner.m_shirtNo + ")</b></div>"); let arr = vanGmMz.mEvent.dataByPlayer[this.pid].data; for (let k = 0; k < arr.length; k++) { let key = 'gw_player_' + pid + "_s_" + k; $(this.divname).append('<div><b id="' + key + '" class="gw_run">' + MyGame.prototype.mzlive.m_match.frameToMatchMinute(arr[k].m_frame_start) + "′[" + arr[k].m_frame_start + "+" + (arr[k].m_frame_end - arr[k].m_frame_start + 1) + "]" + (arr[k].FoolCount == 0 ? "" : ("+" + arr[k].FoolCount)) + " " + vanGmMz.getMatchStatusName(arr[k].status) + "</b></div>"); let dom = $("#" + key)[0]; dom.m_frame = arr[k].m_frame_start; dom.addEventListener('click', function () { MyGame.prototype.mzlive.m_match.setCurrentFrame(this.m_frame); }); } }); } } } , getMatchStatusName: function (status) { switch (status) { case MatchStatus.BA_NORMAL: return vanGmMz.now_language.BA_NORMAL; case MatchStatus.BA_WALL: return vanGmMz.now_language.BA_WALL; case MatchStatus.BA_HOLD: return vanGmMz.now_language.BA_HOLD; case MatchStatus.BA_DOWN: return vanGmMz.now_language.BA_DOWN; case MatchStatus.BA_HOLD_THROWIN: return vanGmMz.now_language.BA_HOLD_THROWIN; case MatchStatus.BA_THROWIN: return vanGmMz.now_language.BA_THROWIN; case MatchStatus.BA_LEFT_FOOT_SHOT_FWD: return vanGmMz.now_language.BA_LEFT_FOOT_SHOT_FWD; case MatchStatus.BA_LEFT_FOOT_SHOT_BACK: return vanGmMz.now_language.BA_LEFT_FOOT_SHOT_BACK; case MatchStatus.BA_LEFT_FOOT_SHOT_RIGHT: return vanGmMz.now_language.BA_LEFT_FOOT_SHOT_RIGHT; case MatchStatus.BA_LEFT_FOOT_SHOT_LEFT: return vanGmMz.now_language.BA_LEFT_FOOT_SHOT_LEFT; case MatchStatus.BA_RIGHT_FOOT_SHOT_FWD: return vanGmMz.now_language.BA_RIGHT_FOOT_SHOT_FWD; case MatchStatus.BA_RIGHT_FOOT_SHOT_BACK: return vanGmMz.now_language.BA_RIGHT_FOOT_SHOT_BACK; case MatchStatus.BA_RIGHT_FOOT_SHOT_RIGHT: return vanGmMz.now_language.BA_RIGHT_FOOT_SHOT_RIGHT; case MatchStatus.BA_RIGHT_FOOT_SHOT_LEFT: return vanGmMz.now_language.BA_RIGHT_FOOT_SHOT_LEFT; case MatchStatus.BA_LEFT_FOOT_PASS_FWD: return vanGmMz.now_language.BA_LEFT_FOOT_PASS_FWD; case MatchStatus.BA_LEFT_FOOT_PASS_BACK: return vanGmMz.now_language.BA_LEFT_FOOT_PASS_BACK; case MatchStatus.BA_LEFT_FOOT_PASS_RIGHT: return vanGmMz.now_language.BA_LEFT_FOOT_PASS_RIGHT; case MatchStatus.BA_LEFT_FOOT_PASS_LEFT: return vanGmMz.now_language.BA_LEFT_FOOT_PASS_LEFT; case MatchStatus.BA_RIGHT_FOOT_PASS_FWD: return vanGmMz.now_language.BA_RIGHT_FOOT_PASS_FWD; case MatchStatus.BA_RIGHT_FOOT_PASS_BACK: return vanGmMz.now_language.BA_RIGHT_FOOT_PASS_BACK; case MatchStatus.BA_RIGHT_FOOT_PASS_RIGHT: return vanGmMz.now_language.BA_RIGHT_FOOT_PASS_RIGHT; case MatchStatus.BA_RIGHT_FOOT_PASS_LEFT: return vanGmMz.now_language.BA_RIGHT_FOOT_PASS_LEFT; case MatchStatus.BA_PICK_UP_BALL: return vanGmMz.now_language.BA_PICK_UP_BALL; case MatchStatus.BA_DROP_BALL: return vanGmMz.now_language.BA_DROP_BALL; case MatchStatus.BA_HEADER: return vanGmMz.now_language.BA_HEADER; case MatchStatus.BA_TRIP: return vanGmMz.now_language.BA_TRIP; case MatchStatus.BA_CELEBRATE: return vanGmMz.now_language.BA_CELEBRATE; case MatchStatus.BA_GK_READY: return vanGmMz.now_language.BA_GK_READY; case MatchStatus.BA_GK_ACRO_LEFT: return vanGmMz.now_language.BA_GK_ACRO_LEFT; case MatchStatus.BA_GK_ACRO_LEFT_HOLD: return vanGmMz.now_language.BA_GK_ACRO_LEFT_HOLD; case MatchStatus.BA_GK_ACRO_RIGHT: return vanGmMz.now_language.BA_GK_ACRO_RIGHT; case MatchStatus.BA_GK_ACRO_RIGHT_HOLD: return vanGmMz.now_language.BA_GK_ACRO_RIGHT_HOLD; case MatchStatus.BA_GK_SIDESTEP_LEFT: return vanGmMz.now_language.BA_GK_SIDESTEP_LEFT; case MatchStatus.BA_GK_SIDESTEP_RIGHT: return vanGmMz.now_language.BA_GK_SIDESTEP_RIGHT; case MatchStatus.BA_GK_KICK: return vanGmMz.now_language.BA_GK_KICK; case MatchStatus.BA_GK_THROW_BALL: return vanGmMz.now_language.BA_GK_THROW_BALL; case MatchStatus.BA_GK_STRETCH_LEFT: return vanGmMz.now_language.BA_GK_STRETCH_LEFT; case MatchStatus.BA_GK_STRETCH_LEFT_HOLD: return vanGmMz.now_language.BA_GK_STRETCH_LEFT_HOLD; case MatchStatus.BA_GK_STRETCH_RIGHT: return vanGmMz.now_language.BA_GK_STRETCH_RIGHT; case MatchStatus.BA_GK_STRETCH_RIGHT_HOLD: return vanGmMz.now_language.BA_GK_STRETCH_RIGHT_HOLD; case MatchStatus.BA_BALL_OWNER: return vanGmMz.now_language.BA_BALL_OWNER; case MatchStatus.BA_TACKLE: return vanGmMz.now_language.BA_TACKLE; case MatchStatus.BA_SLIDING_TACKLE: return vanGmMz.now_language.BA_SLIDING_TACKLE; case MatchStatus.BA_SLIDING_TACKLE_STAND: return vanGmMz.now_language.BA_SLIDING_TACKLE_STAND; case MatchStatus.BA_MAX: return vanGmMz.now_language.BA_MAX; case 1001: return vanGmMz.now_language.BA_MY_1001; case 1002: return vanGmMz.now_language.BA_MY_1002; case 1003: return vanGmMz.now_language.BA_MY_1003; case 1011: return vanGmMz.now_language.BA_MY_1011; case 1012: return vanGmMz.now_language.BA_MY_1012; default: return vanGmMz.now_language.Unknown; } } , StatsToPos_X: function (i, IsLocal) { let ret = IsLocal ? Math.round(-.255800462 * i + 199.8228530689) : Math.round(.2555000556 * i + 8.3741302936); return ret; } , StatsToPos_Y: function (i, IsLocal) { let ret = IsLocal ? Math.round(-.3073207154 * i + 315.9278777381) : Math.round(.3070644902 * i + 9.2794889414); return ret; } , CopyXML: function (mid, ishome) { let xml_mode = GM_getValue("xml_mode", 0); if (xml_mode == 0) { vanGmMz.getMax(function () { let tmpXML = vanGmMz.Stats2XML(mid, ishome, vanGmMz.pmax); GM_setClipboard(tmpXML); alert(vanGmMz.now_language.CopyXmlMsg); }); } else { // The old tuned mode uploaded raw player-page HTML to a third party. // Resolve our player IDs and generate the XML entirely in-browser. vanGmMz.getMax(function () { let tmpXML = vanGmMz.Stats2XML(mid, ishome, vanGmMz.pmax); GM_setClipboard(tmpXML); alert(vanGmMz.now_language.CopyXmlMsg); }); } } , Stats2XML: function (mid, ishome, players) { let data = window["gmXml_" + mid]; if (!data) { return ""; } let teams = data.documentElement.getElementsByTagName("Team"); let team = ishome ? teams[0] : teams[1]; let pidArr = new Array(); if (players) { for (let pid in players) { pidArr.push(pid); } } while (pidArr.length < 11) { pidArr.push(0); } let tmpXML = "<?xml version=\"1.0\" ?>" + "\r\n<SoccerTactics>\r\n\t<Team tactics=" + "\"" + team.getAttribute("tactic") + "\" playstyle=\"" + team.getAttribute("playstyle") + "\" aggression=\"" + team.getAttribute("aggression") + "\" />\r\n" + "\t<Pos pos=\"goalie\" pid=\"" + pidArr.shift() + "\" x=\"103\" y=\"315\" x1=\"103\" y1=\"315\" x2=\"103\" y2=\"315\" pt=\"15\" fk=\"15\" />\r\n"; let players_xml = data.documentElement.getElementsByTagName('Player'); for (var i = 0; i < players_xml.length; i++) { let pl = players_xml[i]; let origin = pl.getAttribute('origin'); let teamId = pl.getAttribute("teamId"); if (origin != "" && origin != "375,0" && origin != "375,1000") { let arr = origin.split(","); if (team.getAttribute("id") == teamId) { let x = vanGmMz.StatsToPos_X(arr[0], ishome); let y = vanGmMz.StatsToPos_Y(arr[1], ishome); tmpXML += "\t<Pos pos=\"normal\" pid=\"" + pidArr.shift() + "\" x=\"" + x + "\" y=\"" + y + "\" x1=\"" + x + "\" y1=\"" + y + "\" x2=\"" + x + "\" y2=\"" + y + "\" pt=\"1\" fk=\"1\" />\r\n"; } } } tmpXML += "</SoccerTactics>\r\n"; return tmpXML; } , _open: undefined, _prepareTransferData: undefined, _centerPowerbox: undefined, _ajaxSubmit: undefined, _getPlayerInfo: undefined, finalInitAfterLoading: undefined, processButtonPresses: undefined, Load010SetupMainSceneInstance: undefined, OK_2D: false, run_Tac: function (playerid) { vanGmMz.getMax(function () { let players = $("#playerInfoWindow"); if (players.length > 0) { let pdom = players.eq(0); let pid = pdom.html().match(vanGmMzModel.mzreg.playerId_tac)[1]; if (pid != playerid) { return; } let player = vanGmMz.pmax[pid]; let imgs = pdom.find("img.skill"); if (player) { vanGmMz.setPlayerImgs(imgs, player); let p_age = teamTactic.tacticsData.TeamPlayers.Player[teamTactic.tacticsData.playerIndexReference[pid]]["@attributes"].age; if (mz.season - p_age >= 52) { vanGmMz.getScoutReport(pid, pdom); } let sp = $("span.clippable.bold"); sp.unbind("click"); sp.bind("click", function () { vanGmMz.showScore(pid, pdom); }); } } }); } , run_Training: function (pid) { vanGmMz.getMax(function () { let players = $("div.tooltip.shadow"); if (players.length > 0) { let pdom = players.eq(0); let player = vanGmMz.pmax[pid]; if (player) { let imgs = pdom.find("img.skill"); vanGmMz.setPlayerImgs(imgs, player); let p_age = players.find(".box_dark").find("p").eq(0).html().match(/\d+/)[0] if (mz.season - p_age >= 52) { vanGmMz.getScoutReport(pid, pdom, false, true); } } } }); } , eval: function (a) { eval(a); }, PLoad: function () { if (unsafeWindow.ajaxSport) { vanGmMz.PLoadInternal(); } else { setTimeout(function () { vanGmMz.PLoad(); }, 1000); } }, PLoadInternal: function () { if ($(".fa").css("font-family") != "FontAwesome") { var style = document.createElement('link'); style.href = 'https://cdnjs.loli.net/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'; style.rel = 'stylesheet'; style.type = 'text/css'; document.getElementsByTagName('HEAD').item(0).appendChild(style); } if (ajaxSport && ajaxSport == "soccer") { let vgm = this; vgm.initgw(); vgm._open = window.XMLHttpRequest.prototype.open; window.XMLHttpRequest.prototype.open = function () { if (vanGmMzModel.mzreg.data2d_url.test(arguments[1])) { vgm.OK_2D = false; if (MyGame.prototype.mzlive.R_GW == undefined) { MyGame.prototype.mzlive.R_GW = true; vgm.finalInitAfterLoading = MyGame.prototype.mzlive.finalInitAfterLoading; MyGame.prototype.mzlive.finalInitAfterLoading = function () { vgm.finalInitAfterLoading.apply(this); vgm.OK_2D = true; vgm.Advanced2D(); }; vgm.Load010SetupMainSceneInstance = MyGame.prototype.Load010SetupMainSceneInstance; MyGame.prototype.Load010SetupMainSceneInstance = function () { window.matchLoader = arguments[0]; vgm.Load010SetupMainSceneInstance.apply(this, arguments); }; } } return vgm._open.apply(this, arguments); }; if (unsafeWindow.prepareTransferData != undefined) { vgm._prepareTransferData = unsafeWindow.prepareTransferData; unsafeWindow.prepareTransferData = function (readyState, response, responseParameter) { vgm._prepareTransferData.apply(this, arguments); if (GM_getValue("autoRun1", 1) == 1) { if (typeof (responseParameter) === "undefined" || !responseParameter) { vgm.gw_start(0); } } }; } if (unsafeWindow.centerPowerbox != undefined) { vgm._centerPowerbox = unsafeWindow.centerPowerbox; unsafeWindow.centerPowerbox = function () { vgm._centerPowerbox.apply(this, arguments); if (GM_getValue("autoRun1", 1) == 1) { vgm.gw_start(0); } }; } if ($.fn.ajaxSubmit != undefined) { vgm._ajaxSubmit = $.fn.ajaxSubmit; $.fn.ajaxSubmit = function (options) { if (options.gm_success_is == undefined) { options.gm_success_is = true; options.gm_success = options.success; options.success = function () { options.gm_success.apply(this, arguments); if (GM_getValue("autoRun1", 1) == 1) { vgm.gw_start(0); } }; } vgm._ajaxSubmit.apply(this, arguments); }; } if (location.href == "https://www.managerzone.com/?p=tactics") { if (unsafeWindow.teamTactic.getPlayerInfo != undefined) { vgm._getPlayerInfo = unsafeWindow.teamTactic.getPlayerInfo; unsafeWindow.teamTactic.getPlayerInfo = function () { vgm._getPlayerInfo.apply(this, arguments); if (GM_getValue("autoRun1", 1) == 1) { vgm.run_Tac(arguments[0]); } }; } } else if (location.href == "https://www.managerzone.com/?p=training") { if (unsafeWindow.displayAndAdjustTooltipPositions != undefined) { vgm._displayAndAdjustTooltipPositions = unsafeWindow.displayAndAdjustTooltipPositions; unsafeWindow.displayAndAdjustTooltipPositions = function () { vgm._displayAndAdjustTooltipPositions.apply(this, arguments); if (GM_getValue("autoRun1", 1) == 1) { vgm.run_Training(arguments[3]); } }; } } vgm.gw_start(0); vple.autoclearCache(); } else if (ajaxSport == "hockey") { let vgm = this; vgm.now_sport = "hockey"; vgm.initgw(); if (unsafeWindow.prepareTransferData != undefined) { vgm._prepareTransferData = unsafeWindow.prepareTransferData; unsafeWindow.prepareTransferData = function (readyState, response, responseParameter) { vgm._prepareTransferData.apply(this, arguments); if (GM_getValue("autoRun1", 1) == 1) { if (typeof (responseParameter) === "undefined" || !responseParameter) { vgm.gw_start(0); } } }; } if (unsafeWindow.centerPowerbox != undefined) { vgm._centerPowerbox = unsafeWindow.centerPowerbox; unsafeWindow.centerPowerbox = function () { vgm._centerPowerbox.apply(this, arguments); if (GM_getValue("autoRun1", 1) == 1) { vgm.gw_start(0); } }; } if ($.fn.ajaxSubmit != undefined) { vgm._ajaxSubmit = $.fn.ajaxSubmit; $.fn.ajaxSubmit = function (options) { if (options.gm_success_is == undefined) { options.gm_success_is = true; options.gm_success = options.success; options.success = function () { options.gm_success.apply(this, arguments); if (GM_getValue("autoRun1", 1) == 1) { vgm.gw_start(0); } }; } vgm._ajaxSubmit.apply(this, arguments); }; } if (location.href == "https://www.managerzone.com/?p=tactics") { if (unsafeWindow.teamTactic.getPlayerInfo != undefined) { vgm._getPlayerInfo = unsafeWindow.teamTactic.getPlayerInfo; unsafeWindow.teamTactic.getPlayerInfo = function () { vgm._getPlayerInfo.apply(this, arguments); if (GM_getValue("autoRun1", 1) == 1) { vgm.run_Tac(arguments[0]); } }; } } vgm.gw_start(0); vple.autoclearCache(); } } , D_GetNowSeasonInfo: function (xPlotLines) { return undefined; }, D_FillTraining: function (type, playerTS, g, NowSeasonInfo) { if (type == "") { vanGmMz.fillTrainingLevel("itc", vanGmMzModel.mzreg.bar_itc, playerTS, g.marker.symbol); vanGmMz.fillTrainingLevel("ycc", vanGmMzModel.mzreg.bar_ycc, playerTS, g.marker.symbol); vanGmMz.fillTrainingLevel("pos", vanGmMzModel.mzreg.bar_pos, playerTS, g.marker.symbol); vanGmMz.fillTrainingLevel("neg", vanGmMzModel.mzreg.bar_neg, playerTS, g.marker.symbol, true); } else { vanGmMz.fillTrainingLevel(type, vanGmMzModel.mzreg.bar_itc, playerTS, g.marker.symbol); vanGmMz.fillTrainingLevel(type, vanGmMzModel.mzreg.bar_ycc, playerTS, g.marker.symbol); vanGmMz.fillTrainingLevel(type, vanGmMzModel.mzreg.bar_pos, playerTS, g.marker.symbol); vanGmMz.fillTrainingLevel(type, vanGmMzModel.mzreg.bar_neg, playerTS, g.marker.symbol, true); } }, D_NowSeasonText: function (pid, NowSeasonInfo, pdom) { }, D_ShowScoutText: function (strSus, pid, pdom, HS, HPids, LS, LPids) { showHelpLayer(strSus, vanGmMz.now_language.scoutReport, true); return strSus; }, D_ShowMaybeSkill: function (pdom, HStar, HP1, HP2, LStar, LP1, LP2) { }, D_OpenSetting: function () { }, D_SetMaybeSkill: function (pdom, HStar, HP1, HP2, LStar, LP1, LP2) { let imgs = pdom.find("img.skill"); let LMax = 10, Nmin = 4, NMax = 10, Hmin = 7; if (LStar == 1) { Nmin = 4; LMax = 6; } else if (LStar == 2) { Nmin = 5; LMax = 8; } else if (LStar == 3) { Nmin = 7; LMax = 10; } if (HStar < 4) { NMax = 9; } let tmpMSkills = {}; for (let i = 0; i < 11; i++) { if (imgs[i].skill == undefined) { return; } let mskill = imgs[i].skill; if (imgs[i].maxed == "green") { mskill += 1; } tmpMSkills[i] = mskill; if (i == LP1 - 1 || i == LP2 - 1) { if (Nmin < mskill) { Nmin = mskill; } } else if (i == HP1 - 1 || i == HP2 - 1) { //高星 if (imgs[i].maxed == "red") { if (mskill < NMax) { NMax = mskill; } } } else { if (Hmin < mskill) { Hmin = mskill; } if (imgs[i].maxed == "red") { if (mskill < LMax) { LMax = mskill; } } } } for (let i = 0; i < 11; i++) { let mbskill = 4; let mbmax = 10; if (i == HP1 - 1 || i == HP2 - 1) { //高星 if (imgs[i].maxed != "red") { mbmax = 10; } if (HStar == 3) { mbskill = 8; } else if (HStar == 4) { mbskill = 9; //h4 9死 另一个一定10 let ahp; if (i == HP1 - 1) { ahp = HP2 - 1; } else { ahp = HP1 - 1; } if (imgs[ahp].maxed == "red" && imgs[ahp].skill == 9) { mbskill = 10; } } if (mbskill < Hmin) { mbskill = Hmin; } if (mbskill < Nmin) { mbskill = Nmin; } } else if (i == LP1 - 1 || i == LP2 - 1) { //低星 if (imgs[i].maxed != "red") { mbmax = LMax; } } else { if (imgs[i].maxed != "red") { mbmax = NMax; } if (mbskill < Nmin) { mbskill = Nmin; } } if (mbskill < tmpMSkills[i]) { mbskill = tmpMSkills[i]; } if (imgs[i].maxed == "red") { mbmax = imgs[i].skill; } imgs[i].mbskill = mbskill; imgs[i].mbmax = mbmax; } }, radarOption: function (tacPlayer, option, text) { let CFScore = this.GetScore(tacPlayer, vanGmMz.tacCof["CF"]); let WFScore = this.GetScore(tacPlayer, vanGmMz.tacCof["LWF"]); //let WMFScore = this.GetScore(tacPlayer, vanGmMz.tacCof["LMF"]); let CMFScore = this.GetScore(tacPlayer, vanGmMz.tacCof["CMF"]); let WMScore = this.GetScore(tacPlayer, vanGmMz.tacCof["LM"]); let CDMScore = this.GetScore(tacPlayer, vanGmMz.tacCof["CDM"]); let WBScore = this.GetScore(tacPlayer, vanGmMz.tacCof["LSB"]); let CBScore = this.GetScore(tacPlayer, vanGmMz.tacCof["CB"]); let GKScore = this.GetScore(tacPlayer, vanGmMz.tacCof["GK"]); option.legend.data.push(text); option.series[0].data.push({ value: [CFScore, WFScore, CDMScore, WBScore, CBScore, GKScore, WMScore, CMFScore], name: text }); }, getRadarOption: function (tacPI, pdom) { let option = { title: { text: vanGmMz.now_language.PosScores }, tooltip: {}, legend: { data: [] }, radar: { name: { textStyle: { color: '#fff', backgroundColor: '#999', borderRadius: 3, padding: [3, 5] } }, indicator: [ { name: vanGmMz.now_language.Pos56, max: 100 }, { name: vanGmMz.now_language.Pos10, max: 100 }, { name: vanGmMz.now_language.Pos21, max: 100 }, { name: vanGmMz.now_language.Pos22, max: 100 }, { name: vanGmMz.now_language.Pos9, max: 100 }, { name: vanGmMz.now_language.Pos7, max: 100 }, { name: vanGmMz.now_language.Pos4, max: 100 }, { name: vanGmMz.now_language.Pos23, max: 100 } ] }, series: [{ name: vanGmMz.now_language.Scores, type: 'radar', data: [] }] }; this.radarOption(tacPI.tacPlayer, option, vanGmMz.now_language.now); let imgs = pdom.find("img.skill"); if (tacPI.tacPlayerMB.InitByImgs(imgs, "mbskill", 100, 100)) { this.radarOption(tacPI.tacPlayerMB, option, vanGmMz.now_language.future); } if (tacPI.tacPlayerMax.InitByImgs(imgs, "mbmax", 100, 100)) { this.radarOption(tacPI.tacPlayerMax, option, vanGmMz.now_language.max); } return option; }, showScore: function (pid, pdom) { let tacPI = this.tacP[pid]; if (tacPI == undefined) { tacPI = { tacPlayer: new vpleModel.TacPlayer(), tacPlayerMB: new vpleModel.TacPlayer(), tacPlayerMax: new vpleModel.TacPlayer() }; this.tacP[pid] = tacPI; } if (tacPI.tacPlayer.InitByTds(pdom.find("td.skillval"))) { let content = "<div class='clearfix'><div id='ldchart' style='height: 400px; width: 400px'></div>"; let pname = pdom.find(".player_name").html(); if (pname == undefined) { pname = pdom.find(".player_link").html(); } showHelpLayer(content, pname + "(" + pid + ")", true); let radar_chart = echarts.init(document.getElementById("ldchart")); radar_chart.setOption(this.getRadarOption(tacPI, pdom)); } }, GetScore: function (player, tacConfItem) { var Speed = this.GetScoreItem(player.Speed, tacConfItem.Speed); var Stamina = this.GetScoreItem(player.Stamina, tacConfItem.Stamina); var Gameintelligence = this.GetScoreItem(player.Gameintelligence, tacConfItem.Gameintelligence); var Passing = this.GetScoreItem(player.Passing, tacConfItem.Passing); var Shooting = this.GetScoreItem(player.Shooting, tacConfItem.Shooting); var Heading = this.GetScoreItem(player.Heading, tacConfItem.Heading); var Goalkeeping = this.GetScoreItem(player.Goalkeeping, tacConfItem.Goalkeeping); var Technique = this.GetScoreItem(player.Technique, tacConfItem.Technique); var Tackling = this.GetScoreItem(player.Tackling, tacConfItem.Tackling); var Highpassing = this.GetScoreItem(player.Highpassing, tacConfItem.Highpassing); var Situations = this.GetScoreItem(player.Situations, tacConfItem.Situations); var Experience = this.GetScoreItem(player.Experience, tacConfItem.Experience); var Form = this.GetScoreItem(player.Form, tacConfItem.Form); var score = player.Speed * Speed + player.Stamina * Stamina + player.Gameintelligence * Gameintelligence + player.Passing * Passing + player.Shooting * Shooting + player.Heading * Heading + player.Goalkeeping * Goalkeeping + player.Technique * Technique + player.Tackling * Tackling + player.Highpassing * Highpassing + player.Situations * Situations + player.Form * Form + player.Experience * Experience; var sum = Speed + Stamina + Gameintelligence + Passing + Shooting + Heading + Goalkeeping + Technique + Tackling + Highpassing + Situations + Form + Experience; return (score / sum).toFixed(2); }, GetScoreItem: function (attrVal, prop) { var lastVal = 0; $.each(prop, function (key, values) { lastVal = values; if (attrVal < key) { return false; } }); return lastVal; } }; (function () { 'use strict'; try { vanGmMz.PLoad(); } catch (e) { console.error('[mzc-adv] van bootstrap threw (skill-coloring/other van features may be down — report this):', e); } })(); // Surface any later async throw from van's features in a clearly tagged way so we // can pinpoint what current-MZ change broke it (does NOT affect the module below). try { window.addEventListener('error', function (ev) { var s = String((ev && ev.error && ev.error.stack) || (ev && ev.message) || ''); if (/vanGmMz|playerAdvanced|vple|echarts|MyGame|ajaxSport/i.test(s)) { console.error('[mzc-adv] van runtime error:', ev.message, ev.filename + ':' + ev.lineno); } }); } catch (e) { /* ignore */ } // ===== mzcopilot match-page player VALUES + team total (independent module) ===== // Self-contained — runs on ?p=match regardless of whether van's code breaks. Adds // each player's value into the Estadísticas stats tables + a per-team total. Values // come from MZ's OWN converted ?p=players&sub=alt page (already in the viewer's // currency), NEVER the raw team_playerlist XML (team's LOCAL currency → inflates a // foreign side). The team total sums ONLY the 11 starters (subs are shown, dimmed). (function () { 'use strict'; var CACHE = {}, running = false; function urlParam(n) { try { return new URL(location.href).searchParams.get(n); } catch (e) { return null; } } function group(n) { return String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, " "); } function parseValor(html) { var out = {}, rowRe = /<tr\b[^>]*>([\s\S]*?)<\/tr>/gi, rm; while ((rm = rowRe.exec(html))) { var row = rm[1] || "", pidM = row.match(/[?&]pid=(\d+)/); if (!pidM) continue; var cellRe = /<td\b[^>]*>([\s\S]*?)<\/td>/gi, cells = [], nameIdx = -1, cm; while ((cm = cellRe.exec(row))) { if (nameIdx === -1 && /[?&]pid=\d+/.test(cm[1])) nameIdx = cells.length; cells.push((cm[1] || "").replace(/<[^>]*>/g, " ").replace(/ /gi, " ").replace(/\s+/g, " ").trim()); } if (nameIdx === -1) continue; var v = Number((cells[nameIdx + 1] || "").replace(/[^\d]/g, "")); if (v > 1000 && out[pidM[1]] == null) out[pidM[1]] = v; } return out; } function fetchTeam(tid) { if (CACHE[tid] && Date.now() - CACHE[tid].at < 1800000) return Promise.resolve(CACHE[tid].map); return fetch("/?p=players&sub=alt&tid=" + encodeURIComponent(tid) + "&sport=soccer", { credentials: "include" }) .then(function (r) { return r.text(); }) .then(function (h) { var m = parseValor(h); CACHE[tid] = { at: Date.now(), map: m }; return m; }) .catch(function (e) { console.warn("[mzc-match] alt-page fetch failed for team", tid, e); return {}; }); } function teamIds() { var ids = [], links = document.querySelectorAll('a[href*="p=team&tid="]'); for (var i = 0; i < links.length; i++) { var m = (links[i].getAttribute("href") || "").match(/tid=(\d+)/); if (m && ids.indexOf(m[1]) < 0) ids.push(m[1]); if (ids.length >= 2) break; } return ids; } function run() { if (urlParam("p") !== "match" || running) return; var tables = [].slice.call(document.querySelectorAll("table.matchStats")).filter(function (t) { return !t.dataset.mzcVal; }); if (!tables.length) return; running = true; var ids = teamIds(); if (!ids.length) { running = false; console.warn("[mzc-match] no team ids on page"); return; } Promise.all(ids.map(fetchTeam)).then(function (maps) { var val = {}; maps.forEach(function (m) { for (var p in m) val[p] = m[p]; }); console.log("[mzc-match] " + Object.keys(val).length + " player values; injecting into " + tables.length + " tables"); tables.forEach(function (T) { if (T.dataset.mzcVal) return; var total = 0, starters = 0; [].slice.call(T.querySelectorAll("tr")).forEach(function (r) { var link = r.querySelector('a[href*="pid="]'), txt = r.textContent || ""; if (link) { starters++; var isStarter = starters <= 11; var m = (link.getAttribute("href") || "").match(/pid=(\d+)/), v = m ? val[m[1]] : null; var td = document.createElement("td"); td.style.cssText = "text-align:right;font-weight:bold;color:#1a7a3a;white-space:nowrap;padding:0 6px"; td.textContent = v != null ? group(v) : "—"; if (isStarter && v != null) total += v; if (!isStarter) { td.style.opacity = "0.45"; td.title = "Suplente — no suma al total"; } r.insertBefore(td, r.cells[1] || null); } else if (/Equipo/i.test(txt)) { var tdt = document.createElement("td"); tdt.style.cssText = "text-align:right;font-weight:bold;color:#b45309;white-space:nowrap;padding:0 6px"; tdt.textContent = group(total); tdt.title = "Suma del 11 titular"; r.insertBefore(tdt, r.cells[1] || null); } else if (r.querySelectorAll("th").length >= 3) { var th = document.createElement("th"); th.textContent = "Valor"; r.insertBefore(th, r.cells[1] || null); } }); T.dataset.mzcVal = "1"; }); }).catch(function (e) { console.warn("[mzc-match] inject error", e); }).then(function () { running = false; }); } function schedule() { [800, 2000, 3500, 6000].forEach(function (ms) { setTimeout(run, ms); }); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", schedule); else schedule(); var dbnc; try { new MutationObserver(function () { clearTimeout(dbnc); dbnc = setTimeout(run, 700); }).observe(document.documentElement, { childList: true, subtree: true }); } catch (e) { /* ignore */ } })(); // ===== mzcopilot match INSIGHT (?p=match) — per-team starting-XI rating summary ===== // Self-contained: reads the per-player ratings already in MZ's lineup tables // (table.statsLite.marker, rating = last cell) and shows, per team, the XI // average rating + MVP (top-rated) + weakest starter. No account/API needed. (function () { 'use strict'; function up(n) { try { return new URL(location.href).searchParams.get(n); } catch (e) { return null; } } function teamNames() { var m = (document.title || '').match(/\|\s*(.+?)\s+-\s+(.+?)\s*\(/); return m ? [m[1].trim(), m[2].trim()] : ['Local', 'Visitante']; } var building = false; function pVal(html) { var out = {}, R = /<tr\b[^>]*>([\s\S]*?)<\/tr>/gi, m; while ((m = R.exec(html))) { var r = m[1] || "", p = r.match(/[?&]pid=(\d+)/); if (!p) continue; var C = /<td\b[^>]*>([\s\S]*?)<\/td>/gi, c = [], n = -1, x; while ((x = C.exec(r))) { if (n < 0 && /[?&]pid=\d+/.test(x[1])) n = c.length; c.push((x[1] || "").replace(/<[^>]*>/g, " ").replace(/ /gi, " ").replace(/\s+/g, " ").trim()); } if (n < 0) continue; var v = Number((c[n + 1] || "").replace(/[^\d]/g, "")); if (v > 1000) out[p[1]] = v; } return out; } async function run() { if (building || up('p') !== 'match' || document.getElementById('mzc-insight')) return; var tables = [].slice.call(document.querySelectorAll('table.statsLite.marker')).filter(function (t) { return t.querySelector('a[href*="pid="]'); }); if (!tables.length) return; building = true; try { var names = teamNames(), insights = [], mid = up('mid'); tables.forEach(function (t, idx) { var rows = [].slice.call(t.querySelectorAll('tr')).filter(function (r) { return r.querySelector('a[href*="pid="]'); }); var players = rows.map(function (r) { var c = r.cells, a = r.querySelector('a[href*="pid="]'); return { name: a.textContent.trim(), pid: (a.getAttribute('href').match(/pid=(\d+)/) || [])[1], rating: parseFloat((c[c.length - 1].textContent || '').replace(',', '.')) }; }).filter(function (p) { return !isNaN(p.rating) && p.rating > 0; }); var xi = players.slice(0, 11); if (!xi.length) return; var s = xi.slice().sort(function (a, b) { return b.rating - a.rating; }); var avg = (xi.reduce(function (a, b) { return a + b.rating; }, 0) / xi.length).toFixed(2); insights.push({ team: names[idx] || ('Equipo ' + (idx + 1)), avg: avg, mvp: s[0], worst: s[s.length - 1], pids: xi.map(function (p) { return p.pid; }) }); }); if (!insights.length) { building = false; return; } // starting-11 $ value: fetch both teams' converted alt pages, merge, sum each XI var ids = []; document.querySelectorAll('a[href*="p=team&tid="]').forEach(function (a) { var m = (a.getAttribute('href') || '').match(/tid=(\d+)/); if (m && ids.indexOf(m[1]) < 0 && ids.length < 2) ids.push(m[1]); }); var val = {}; try { var maps = await Promise.all(ids.map(function (t) { return fetch('/?p=players&sub=alt&tid=' + t + '&sport=soccer', { credentials: 'include' }).then(function (r) { return r.text(); }).then(pVal).catch(function () { return {}; }); })); maps.forEach(function (mp) { for (var p in mp) val[p] = mp[p]; }); } catch (e) { /* values optional */ } insights.forEach(function (ti) { var sum = ti.pids.reduce(function (a, p) { return a + (val[p] || 0); }, 0); ti.xiVal = sum > 0 ? sum : null; }); if (document.getElementById('mzc-insight')) { building = false; return; } var box = document.createElement('div'); box.id = 'mzc-insight'; box.style.cssText = 'margin:8px auto;max-width:980px;border-radius:11px;overflow:hidden;box-shadow:0 3px 14px rgba(0,0,0,.22);border:1px solid #cbd5e1;font:13px/1.5 Arial,Helvetica,sans-serif;background:#eef1f6'; function teamCard(ti, accent) { return '<div style="background:#fff;border:1px solid #e5e9f0;border-radius:9px;box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:hidden"><div style="padding:6px 11px;background:' + accent + '1a;border-bottom:1px solid ' + accent + '33;font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:.4px;color:' + accent + '">' + ti.team + '</div><div style="padding:9px 11px">' + (ti.xiVal ? '<span style="display:inline-block;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:4px 11px;margin:0 7px 6px 0"><span style="color:#64748b;font-size:10px;text-transform:uppercase;letter-spacing:.5px">Valor XI</span> <b style="color:#1d4ed8;font-size:15px;margin-left:3px">$' + (ti.xiVal / 1e6).toFixed(1) + 'M</b></span>' : '') + '<span style="display:inline-block;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:4px 11px;margin-bottom:6px"><span style="color:#64748b;font-size:10px;text-transform:uppercase;letter-spacing:.5px">Nota media</span> <b style="color:#0f172a;font-size:14px;margin-left:3px">' + ti.avg + '</b></span>' + '<div style="margin-top:4px;font-size:12.5px">⭐ MVP: <b>' + ti.mvp.name + '</b> <span style="color:#16a34a;font-weight:700">' + ti.mvp.rating.toFixed(2) + '</span></div>' + '<div style="margin-top:2px;font-size:12.5px;color:#64748b">▽ Flojo: ' + ti.worst.name + ' <span style="color:#dc2626;font-weight:700">' + ti.worst.rating.toFixed(2) + '</span></div></div></div>'; } // Deep-link to the full server-side movement analysis (heatmaps, distance, // formation, territory) on the mzcopilot app — the engine lives there; this // button is the hybrid entry point from MZ's own match page. var analyticsBtn = mid ? '<a href="https://mzcopilot.vercel.app/match/' + encodeURIComponent(mid) + '#estadisticas" target="_blank" rel="noopener" style="float:right;margin-top:-2px;background:rgba(255,255,255,.18);color:#fff;text-decoration:none;border-radius:7px;padding:3px 11px;font-size:12px;font-weight:700">📊 Mapas de calor & análisis ↗</a>' : ''; box.innerHTML = '<div style="background:linear-gradient(135deg,#1e293b,#2563eb);color:#fff;padding:9px 15px;font-weight:700;letter-spacing:.3px">' + analyticsBtn + '<span style="font-size:15px;margin-right:6px">⚡</span>mzcopilot <span style="opacity:.65;font-weight:400">· análisis del partido</span></div>' + '<div style="padding:12px 13px"><div style="display:grid;grid-template-columns:' + (insights.length > 1 ? '1fr 1fr' : '1fr') + ';gap:11px">' + insights.map(function (ti, i) { return teamCard(ti, i === 0 ? '#2563eb' : '#475569'); }).join('') + '</div></div>'; var stripes = document.getElementById('stripes'); if (stripes) stripes.insertBefore(box, stripes.firstChild); else { var anchor = tables[0].closest('table') || tables[0]; if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(box, anchor); } } catch (e) { /* ignore */ } building = false; } function sched() { [800, 2000, 4000].forEach(function (ms) { setTimeout(run, ms); }); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', sched); else sched(); var d2; try { new MutationObserver(function () { clearTimeout(d2); d2 = setTimeout(run, 700); }).observe(document.documentElement, { childList: true, subtree: true }); } catch (e) { /* ignore */ } })(); // ===== mzcopilot RIVAL BRIEF (?p=team&tid=X) — scout panel ===== // Self-contained: the team's converted player values (alt page) + recent // SENIOR-official form (team_matchlist.php) → XI value, squad size, last-5 form, // top threats. No account/API needed. Fires on any OTHER team's page // (?p=team&tid=X); clubhouse is own-team-only so it can't be used for rivals. (function () { 'use strict'; function up(n) { try { return new URL(location.href).searchParams.get(n); } catch (e) { return null; } } function parseValor(html) { var out = {}, R = /<tr\b[^>]*>([\s\S]*?)<\/tr>/gi, m; while ((m = R.exec(html))) { var r = m[1] || "", p = r.match(/[?&]pid=(\d+)/); if (!p) continue; var C = /<td\b[^>]*>([\s\S]*?)<\/td>/gi, c = [], n = -1, x; while ((x = C.exec(r))) { if (n < 0 && /[?&]pid=\d+/.test(x[1])) n = c.length; c.push((x[1] || "").replace(/<[^>]*>/g, " ").replace(/ /gi, " ").replace(/\s+/g, " ").trim()); } if (n < 0) continue; var name = (c[n] || "").replace(/^\d+/, "").replace(/\s*\d.*$/, "").trim(); // strip shirt# + age/pos tail var v = Number((c[n + 1] || "").replace(/[^\d]/g, "")); if (v > 1000) out[p[1]] = { name: name, v: v }; } return out; } function recentForm(ml, tid) { var out = [], blocks = ml.match(/<Match\b[^>]*>[\s\S]*?<\/Match>/g) || []; for (var i = 0; i < blocks.length && out.length < 5; i++) { var b = blocks[i], head = (b.match(/<Match\b[^>]*>/) || [""])[0]; if (!/status="played"/.test(head)) continue; var type = (head.match(/type="([^"]*)"/) || [])[1] || ""; if (!/^(league|world_league|cup)/.test(type)) continue; // senior official only var teams = b.match(/<Team\b[^>]*>/g) || [], mine = null, opp = null; for (var j = 0; j < teams.length; j++) { var id = (teams[j].match(/teamId="(\d+)"/) || [])[1], g = Number((teams[j].match(/goals="(\d+)"/) || [])[1]); if (id === tid) mine = g; else opp = g; } if (mine == null || opp == null) continue; out.push(mine > opp ? "W" : mine < opp ? "L" : "D"); } return out; } async function run() { var tid = up('tid'); if (up('p') !== 'team' || !tid || document.getElementById('mzc-rival')) return; var ml, alt; try { var res = await Promise.all([ fetch('/xml/team_matchlist.php?sport_id=soccer&team_id=' + tid, { credentials: 'include' }).then(function (r) { return r.text(); }), fetch('/?p=players&sub=alt&tid=' + tid + '&sport=soccer', { credentials: 'include' }).then(function (r) { return r.text(); }) ]); ml = res[0]; alt = res[1]; } catch (e) { console.warn('[mzc-rival] fetch failed', e); return; } var pmap = parseValor(alt); var players = Object.keys(pmap).map(function (k) { return pmap[k]; }).sort(function (a, b) { return b.v - a.v; }); if (!players.length) return; var xi = players.slice(0, 11), xiVal = xi.reduce(function (s, p) { return s + p.v; }, 0); var form = recentForm(ml, tid); var formHtml = form.map(function (r) { var c = r === 'W' ? '#16a34a' : r === 'D' ? '#d97706' : '#dc2626'; return '<span style="display:inline-flex;width:18px;height:18px;align-items:center;justify-content:center;border-radius:4px;color:#fff;font-weight:700;font-size:11px;margin-right:3px;background:' + c + '">' + r + '</span>'; }).join(''); var threatHtml = players.slice(0, 4).map(function (p) { return '<span style="display:inline-block;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:999px;padding:2px 9px;margin:0 5px 4px 0;font-size:12px;color:#334155"><b style="color:#0f172a">' + p.name + '</b> <span style="color:#16a34a;font-weight:600">$' + (p.v / 1e6).toFixed(2) + 'M</span></span>'; }).join(''); var rPill = function (label, val) { return '<span style="display:inline-block;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:3px 11px;margin:0 8px 4px 0"><span style="color:#64748b;font-size:11px;text-transform:uppercase;letter-spacing:.4px">' + label + '</span> <b style="color:#0f172a;font-size:14px">' + val + '</b></span>'; }; var box = document.createElement('div'); box.id = 'mzc-rival'; box.style.cssText = 'margin:8px auto;max-width:980px;border-radius:10px;overflow:hidden;box-shadow:0 2px 10px rgba(0,0,0,.18);border:1px solid #cbd5e1;font:13px/1.5 Arial,Helvetica,sans-serif;background:#fff'; box.innerHTML = '<div style="background:linear-gradient(135deg,#1e293b,#2563eb);color:#fff;padding:8px 14px;font-weight:700;letter-spacing:.3px"><span style="font-size:15px;margin-right:6px">⚡</span>mzcopilot <span style="opacity:.65;font-weight:400">· informe rival</span></div>' + '<div style="padding:11px 14px">' + '<div style="margin-bottom:6px">' + rPill('Valor XI', '$' + (xiVal / 1e6).toFixed(1) + 'M') + rPill('Plantel', players.length) + (form.length ? '<span style="white-space:nowrap"><span style="color:#64748b;font-size:11px;text-transform:uppercase;letter-spacing:.4px;margin-right:5px">Forma</span>' + formHtml + '</span>' : '') + '</div>' + '<div><span style="color:#64748b;font-size:11px;text-transform:uppercase;letter-spacing:.4px">Amenazas</span><div style="margin-top:4px">' + threatHtml + '</div></div>' + '</div>'; var anchor = document.getElementById('stripes') || document.getElementById('mainDiv') || document.body; anchor.insertBefore(box, anchor.firstChild); } function sched() { [1000, 2500, 4500].forEach(function (ms) { setTimeout(run, ms); }); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', sched); else sched(); })(); // ===== mzcopilot LINEUP CHECK (?p=tactics) — XI value + legality ===== // Self-contained: reads the placed XI from the tactics pitch (#pitch drag tokens; // subs carry .substitute), your converted squad values (own alt page), and shows the // starting-XI value, formation, an 11/11 legality check, and your priciest sub. // Purely additive + fully try/caught: on any failure it shows nothing and never // touches van's tactics coloring. The badge is a SIBLING of #pitch and we observe // only #pitch, so re-rendering the badge can't retrigger itself (no mutation loop). (function () { 'use strict'; var VMAP = null, building = false, observing = false; function up(n) { try { return new URL(location.href).searchParams.get(n); } catch (e) { return null; } } function parseValor(html) { var out = {}, R = /<tr\b[^>]*>([\s\S]*?)<\/tr>/gi, m; while ((m = R.exec(html))) { var r = m[1] || "", p = r.match(/[?&]pid=(\d+)/); if (!p) continue; var C = /<td\b[^>]*>([\s\S]*?)<\/td>/gi, c = [], n = -1, x; while ((x = C.exec(r))) { if (n < 0 && /[?&]pid=\d+/.test(x[1])) n = c.length; c.push((x[1] || "").replace(/<[^>]*>/g, " ").replace(/ /gi, " ").replace(/\s+/g, " ").trim()); } if (n < 0) continue; var name = (c[n] || "").replace(/^\d+/, "").replace(/\s*\d.*$/, "").trim(); var v = Number((c[n + 1] || "").replace(/[^\d]/g, "")); if (v > 1000) out[p[1]] = { name: name, v: v }; } return out; } function getVals() { if (VMAP) return Promise.resolve(VMAP); return fetch('/?p=team', { credentials: 'include' }).then(function (r) { return r.text(); }).then(function (t) { var tid = (t.match(/[?&]tid=(\d+)/) || [])[1]; if (!tid) { VMAP = {}; return VMAP; } return fetch('/?p=players&sub=alt&tid=' + tid + '&sport=soccer', { credentials: 'include' }).then(function (r) { return r.text(); }).then(function (h) { VMAP = parseValor(h); return VMAP; }); }).catch(function () { VMAP = {}; return VMAP; }); } async function run() { if (building || up('p') !== 'tactics') return; var pitch = document.getElementById('pitch'); if (!pitch) return; if (!pitch.querySelector('[id^="drag_n_"]')) return; building = true; try { var vmap = await getVals(); var placed = [].slice.call(pitch.querySelectorAll('[id^="drag_n_"]')); var starterEls = placed.filter(function (e) { return !e.classList.contains('substitute'); }); var subEls = placed.filter(function (e) { return e.classList.contains('substitute'); }); var starters = starterEls.map(function (e) { return e.id.replace('drag_n_', ''); }); var illegal = starterEls.filter(function (e) { return !e.classList.contains('fieldpos-ok'); }).length; var formation = ((document.getElementById('formation_text') || {}).textContent || '').trim(); var xiVal = starters.reduce(function (s, p) { return s + (vmap[p] ? vmap[p].v : 0); }, 0); var bench = subEls.map(function (e) { return vmap[e.id.replace('drag_n_', '')]; }).filter(Boolean).sort(function (a, b) { return b.v - a.v; }); var topSub = bench[0]; var ok = (starterEls.length === 11 && illegal === 0); var warn = (starterEls.length !== 11 ? (starterEls.length + '/11 en cancha') : '') + (illegal ? ((starterEls.length !== 11 ? ' · ' : '') + illegal + ' fuera de posición') : ''); var old = document.getElementById('mzc-lineup'); if (old && old.parentNode) old.parentNode.removeChild(old); var lPill = function (label, val) { return '<span style="display:inline-block;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:3px 11px;margin:0 8px 4px 0"><span style="color:#64748b;font-size:11px;text-transform:uppercase;letter-spacing:.4px">' + label + '</span> <b style="color:#0f172a;font-size:14px">' + val + '</b></span>'; }; var status = ok ? '<span style="display:inline-block;background:#dcfce7;border:1px solid #86efac;border-radius:8px;padding:3px 11px;margin:0 8px 4px 0;color:#15803d;font-weight:700">11/11 ✓</span>' : '<span style="display:inline-block;background:#fef3c7;border:1px solid #fcd34d;border-radius:8px;padding:3px 11px;margin:0 8px 4px 0;color:#b45309;font-weight:700">⚠ ' + warn + '</span>'; var box = document.createElement('div'); box.id = 'mzc-lineup'; box.style.cssText = 'margin:8px auto;max-width:780px;border-radius:10px;overflow:hidden;box-shadow:0 2px 10px rgba(0,0,0,.18);border:1px solid ' + (ok ? '#cbd5e1' : '#f59e0b') + ';font:13px/1.5 Arial,Helvetica,sans-serif;background:#fff'; box.innerHTML = '<div style="background:linear-gradient(135deg,#1e293b,#2563eb);color:#fff;padding:8px 14px;font-weight:700;letter-spacing:.3px"><span style="font-size:15px;margin-right:6px">⚡</span>mzcopilot <span style="opacity:.65;font-weight:400">· alineación</span></div>' + '<div style="padding:11px 14px">' + '<div>' + lPill('Valor XI', xiVal ? ('$' + (xiVal / 1e6).toFixed(1) + 'M') : '—') + (formation ? lPill('Formación', formation) : '') + status + '</div>' + (topSub ? ('<div style="margin-top:2px;color:#64748b;font-size:12px">Suplente más caro: <b style="color:#0f172a">' + topSub.name + '</b> <span style="color:#16a34a;font-weight:600">$' + (topSub.v / 1e6).toFixed(2) + 'M</span></div>') : '') + '</div>'; var tbox = document.getElementById('tactics_box'); if (tbox && tbox.parentNode) tbox.parentNode.insertBefore(box, tbox.nextSibling); else pitch.parentNode.insertBefore(box, pitch); if (!observing) { observing = true; try { var d = null; new MutationObserver(function () { clearTimeout(d); d = setTimeout(run, 800); }).observe(pitch, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] }); } catch (e) { /* ignore */ } } } catch (e) { /* never break the tactics page */ } building = false; } function sched() { [1000, 2500, 4500].forEach(function (ms) { setTimeout(run, ms); }); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', sched); else sched(); })(); // ===== mzcopilot CLUBHOUSE PANEL (?p=clubhouse) — personal dashboard (P1–P3) ===== // READ-ONLY command center on your own clubhouse: squad value, XI value, recent // official form, top assets, and an AUTO-SCOUT of your next opponent (their XI // value + form + threats). Own team id is read dynamically from /?p=team (never // hardcoded) so it works for any user. All data from MZ read-safe endpoints — zero // mutations. Injected at the top of #stripes. Fully try/caught. (function () { 'use strict'; var built = false; function up(n) { try { return new URL(location.href).searchParams.get(n); } catch (e) { return null; } } function parseValor(html) { var out = {}, R = /<tr\b[^>]*>([\s\S]*?)<\/tr>/gi, m; while ((m = R.exec(html))) { var r = m[1] || "", p = r.match(/[?&]pid=(\d+)/); if (!p) continue; var C = /<td\b[^>]*>([\s\S]*?)<\/td>/gi, c = [], n = -1, x; while ((x = C.exec(r))) { if (n < 0 && /[?&]pid=\d+/.test(x[1])) n = c.length; c.push((x[1] || "").replace(/<[^>]*>/g, " ").replace(/ /gi, " ").replace(/\s+/g, " ").trim()); } if (n < 0) continue; var name = (c[n] || "").replace(/^\d+/, "").replace(/\s*\d.*$/, "").trim(); var v = Number((c[n + 1] || "").replace(/[^\d]/g, "")); if (v > 1000) out[p[1]] = { name: name, v: v }; } return out; } function sortedPlayers(html) { var pm = parseValor(html); return Object.keys(pm).map(function (k) { return pm[k]; }).sort(function (a, b) { return b.v - a.v; }); } function recentForm(ml, tid) { var o = [], b = ml.match(/<Match\b[^>]*>[\s\S]*?<\/Match>/g) || []; for (var i = b.length - 1; i >= 0 && o.length < 5; i--) { var h = (b[i].match(/<Match\b[^>]*>/) || [''])[0]; if (!/status="played"/.test(h)) continue; var ty = (h.match(/type="([^"]*)"/) || [])[1] || ""; if (!/^(league|world_league|cup)/.test(ty)) continue; var ts = b[i].match(/<Team\b[^>]*>/g) || [], mine = null, opp = null; for (var j = 0; j < ts.length; j++) { var id = (ts[j].match(/teamId="(\d+)"/) || [])[1], g = Number((ts[j].match(/goals="(\d+)"/) || [])[1]); if (id === tid) mine = g; else opp = g; } if (mine == null || opp == null) continue; o.unshift(mine > opp ? 'W' : mine < opp ? 'L' : 'D'); } return o; } function pill(l, v, em) { return '<span style="display:inline-block;background:' + (em ? '#eff6ff' : '#fff') + ';border:1px solid ' + (em ? '#bfdbfe' : '#e2e8f0') + ';border-radius:8px;padding:4px 11px;margin:0 7px 5px 0"><span style="color:#64748b;font-size:10px;text-transform:uppercase;letter-spacing:.5px">' + l + '</span> <b style="color:' + (em ? '#1d4ed8' : '#0f172a') + ';font-size:14px;margin-left:3px">' + v + '</b></span>'; } function card(icon, title, accent, bodyHtml) { return '<div style="background:#fff;border:1px solid #e5e9f0;border-radius:9px;box-shadow:0 1px 3px rgba(0,0,0,.06);overflow:hidden"><div style="display:flex;align-items:center;gap:6px;padding:6px 11px;background:' + accent + '1a;border-bottom:1px solid ' + accent + '33"><span style="font-size:13px">' + icon + '</span><span style="font-size:10.5px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:' + accent + '">' + title + '</span></div><div style="padding:9px 11px">' + bodyHtml + '</div></div>'; } function M(n) { return '$' + (n / 1e6).toFixed(1) + 'M'; } function K(n) { return n >= 1e6 ? '$' + (n / 1e6).toFixed(1) + 'M' : '$' + Math.round(n / 1e3) + 'K'; } function fsq(f) { return f.map(function (r) { var c = r === 'W' ? '#16a34a' : r === 'D' ? '#d97706' : '#dc2626'; return '<span style="display:inline-flex;width:18px;height:18px;align-items:center;justify-content:center;border-radius:4px;color:#fff;font-weight:700;font-size:11px;margin-right:3px;background:' + c + '">' + r + '</span>'; }).join(''); } function chip(n, v) { return '<span style="display:inline-block;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:999px;padding:2px 9px;margin:0 5px 5px 0;font-size:12px;color:#334155"><b style="color:#0f172a">' + n + '</b> <span style="color:#16a34a;font-weight:600">$' + (v / 1e6).toFixed(2) + 'M</span></span>'; } function panelHead(label) { return '<div style="background:#f8fafc;border:1px solid #eef2f7;border-radius:8px;padding:10px 12px"><div style="font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:#475569;font-weight:700;margin-bottom:5px">' + label + '</div>'; } function gt(u) { return fetch(u, { credentials: 'include' }).then(function (r) { return r.text(); }).catch(function () { return ''; }); } async function run() { if (built || up('p') !== 'clubhouse' || document.getElementById('mzc-panel')) return; var stripes = document.getElementById('stripes'); if (!stripes) return; built = true; try { var teamHtml = await gt('/?p=team'); var tid = (teamHtml.match(/[?&]tid=(\d+)/) || [])[1]; if (!tid) { built = false; return; } var unameEl = document.querySelector('#header-username,.username,#loggedInUsername,.header-user'); var uname = unameEl ? unameEl.textContent.trim() : ''; var res = await Promise.all([ gt('/?p=players&sub=alt&tid=' + tid + '&sport=soccer'), gt('/xml/team_matchlist.php?sport_id=soccer&team_id=' + tid), gt('/?p=match&sub=scheduled'), gt('/xml/team_playerlist.php?sport_id=soccer&team_id=' + tid), gt('/?p=economy'), uname ? gt('/xml/manager_data.php?sport_id=soccer&username=' + encodeURIComponent(uname)) : Promise.resolve('') ]); var players = sortedPlayers(res[0]); if (!players.length) { built = false; return; } var squadVal = players.reduce(function (s, p) { return s + p.v; }, 0); var xiVal = players.slice(0, 11).reduce(function (s, p) { return s + p.v; }, 0); var form = recentForm(res[1], tid); var sd = new DOMParser().parseFromString(res[2], 'text/html'); var oppEl = [].slice.call(sd.querySelectorAll('a[href*="p=team&tid="]')).map(function (a) { return { tid: (a.getAttribute('href').match(/tid=(\d+)/) || [])[1], name: a.textContent.trim() }; }).filter(function (x) { return x.tid && x.tid !== tid && x.name; })[0]; var scoutHtml = '<div style="color:#94a3b8">sin próximo partido</div>', oppName = ''; if (oppEl) { oppName = oppEl.name.length > 22 ? oppEl.name.slice(0, 22) + '…' : oppEl.name; var orr = await Promise.all([gt('/?p=players&sub=alt&tid=' + oppEl.tid + '&sport=soccer'), gt('/xml/team_matchlist.php?sport_id=soccer&team_id=' + oppEl.tid)]); var op = sortedPlayers(orr[0]); if (op.length) { var oXi = op.slice(0, 11).reduce(function (s, p) { return s + p.v; }, 0), oForm = recentForm(orr[1], oppEl.tid); scoutHtml = '<div style="margin-top:3px">XI <b>$' + (oXi / 1e6).toFixed(1) + 'M</b>' + (oForm.length ? ' ' + fsq(oForm) : '') + '</div><div style="margin-top:5px">' + op.slice(0, 3).map(function (p) { return chip(p.name, p.v); }).join('') + '</div>'; } else { scoutHtml = '<div style="color:#94a3b8">sin datos del rival</div>'; } } // P2 data: squad health (injuries), economy, league standing var plP = res[3].match(/<Player\b[^>]*>/g) || []; var squadSize = plP.length || players.length; var injured = plP.filter(function (p) { return /injury="[1-9]/.test(p); }).map(function (p) { return (p.match(/name="([^"]*)"/) || [])[1] || '?'; }); var saludBody = injured.length ? '<span style="display:inline-block;background:#fef3c7;border:1px solid #fcd34d;border-radius:999px;padding:2px 10px;margin:0 6px 4px 0;color:#b45309;font-weight:700;font-size:12px">' + injured.length + ' lesionado' + (injured.length > 1 ? 's' : '') + '</span><span style="color:#64748b;font-size:12px">' + squadSize + ' jugadores</span><div style="margin-top:4px;font-size:12px;color:#334155">' + injured.slice(0, 4).join(' · ') + '</div>' : '<span style="display:inline-block;background:#dcfce7;border:1px solid #86efac;border-radius:999px;padding:2px 10px;margin-right:6px;color:#15803d;font-weight:700;font-size:12px">0 lesionados</span><span style="color:#64748b;font-size:12px">' + squadSize + ' jugadores · 0 suspendidos</span>'; var ecoTxt = ''; try { ecoTxt = new DOMParser().parseFromString(res[4], 'text/html').body.textContent.replace(/\s+/g, ' '); } catch (e) { } function ecoNum(re) { var m = ecoTxt.match(re); return m ? Number(m[1].replace(/[^\d]/g, '')) : null; } var balance = ecoNum(/([\d\s.]+)\s*USD\s*Tu Saldo Actual/i), income = ecoNum(/Ingresos\s*([\d\s.]+)\s*USD/i), expProj = ecoNum(/Tu Saldo Actual\s*([\d\s.]+)\s*USD\s*Proyecci/i); var ecoBody = balance != null ? pill('Balance', M(balance)) + (income != null ? '<span style="font-size:12px;color:#64748b;margin-right:8px">Ingresos <b style="color:#16a34a">+' + K(income) + '</b>/sem</span>' : '') + (expProj != null ? '<span style="font-size:12px;color:#64748b">Gastos <b style="color:#dc2626">-' + K(expProj) + '</b>/sem</span>' : '') : '<span style="color:#94a3b8">sin datos</span>'; var standing = null, seriesId = (res[5].match(/seriesId="(\d+)"/) || [])[1]; if (seriesId) { var lgx = await gt('/xml/team_league.php?sport_id=soccer&league_id=' + seriesId); var ownT = (lgx.match(new RegExp('<Team\\b[^>]*teamId="' + tid + '"[^>]*>')) || [])[0] || ''; if (ownT) standing = { pos: (ownT.match(/pos="(\d+)"/) || [])[1], total: (lgx.match(/<Team\b/g) || []).length, pts: (ownT.match(/points="(\d+)"/) || [])[1] }; } // Native MZ widget chrome: reuse MZ's own .box_dark + .widget-content + h3 // classes so MZ's stylesheet styles us 1:1 with the clubhouse widgets, and // a real .mzbtn green button for the app link. var box = document.createElement('div'); box.id = 'mzc-panel'; box.className = 'flex-grow-1 box_dark'; box.style.cssText = 'margin:8px auto;max-width:980px;float:none;font:13px/1.5 Arial,Helvetica,sans-serif'; var kpiCard = '<div style="background:#fff;border:1px solid #e5e9f0;border-radius:9px;box-shadow:0 1px 3px rgba(0,0,0,.06);padding:9px 11px;margin-bottom:11px">' + pill('Valor plantel', M(squadVal)) + pill('Valor XI', M(xiVal)) + pill('Plantel', players.length) + (standing ? pill('Liga', standing.pos + 'º/' + standing.total + ' · ' + standing.pts + 'pts', true) : '') + (form.length ? '<span style="white-space:nowrap;margin-left:2px"><span style="color:#64748b;font-size:10px;text-transform:uppercase;letter-spacing:.5px;margin-right:5px">Forma</span>' + fsq(form) + '</span>' : '') + '</div>'; box.innerHTML = '<div class="widget-content clearfix"><h3><span style="margin-right:6px">⚡</span>mzcopilot</h3><div class="widget-content-wrapper">' + kpiCard + '<div style="display:grid;grid-template-columns:1fr 1fr;gap:11px">' + card('🎯', 'Próximo rival' + (oppName ? ' · ' + oppName : ''), '#dc2626', scoutHtml) + card('💎', 'Mis cracks', '#2563eb', players.slice(0, 5).map(function (p) { return chip(p.name, p.v); }).join('')) + card('🏥', 'Salud del plantel', '#16a34a', saludBody) + card('💰', 'Economía', '#d97706', ecoBody) + '</div>' + '<div style="margin-top:11px"><a href="https://mzcopilot.vercel.app" target="_blank" rel="noopener" class="mzbtn buttondiv button_account"><span class="buttonClassMiddle"><span style="white-space:nowrap">Abrir mzcopilot app</span></span><span class="buttonClassRight"> </span></a></div>' + '</div></div>'; stripes.insertBefore(box, stripes.firstChild); } catch (e) { built = false; /* never break the clubhouse */ } } function sched() { [900, 2200, 4200].forEach(function (ms) { setTimeout(run, ms); }); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', sched); else sched(); })(); // =========================================================================== // Proyección ("skiller") — clone a player into 2–4 future age stages and preview // their projected skill balls. Manual balls are exact; "Auto" prefills a // calibrated, decelerating estimate (labeled est.) Self-contained IIFE; mounts // on every .playerContainer (own /?p=players cards AND the market playerinfo // popup). No deps beyond the page's jQuery + GM_*. Spec: docs/features/projeccion-skiller-spec.md // =========================================================================== (function () { "use strict"; // Use the bare global $ like the rest of the script. CRITICAL: do NOT gate on // `typeof $` here at IIFE-execution time — the userscript runs BEFORE the page's // jQuery is ready, so a synchronous top guard permanently disables the module // (the mzc14/15/16 bug). $ is only referenced inside the deferred mount() below, // which the interval retries until jQuery exists (same pattern van's code uses). console.log("[mzc-proj] Proyección module loaded (4.28.3-mzc1)"); var MAX_BALLS = 10; // MZ soccer skill ball cap (display). Adjust if MZ differs. // Native ball glyphs (identical base64 to the script's vanGmMzModel.mzImg). var BALL = { b: "data:image/gif;base64,R0lGODlhDAAKAJEDAP///5mZmQAAAP///yH5BAEAAAMALAAAAAAMAAoAAAIk3CIpYZ0BABJtxvjMgojTIVwKpl0dCQbQJX3T+jpLNDXGlDUFADs=", r: "data:image/gif;base64,R0lGODlhDAAKAJEDAP////8AAMyZmf///yH5BAEAAAMALAAAAAAMAAoAAAIk3BQZYp0CAAptxvjMgojTEVwKpl0dCQrQJX3T+jpLNDXGlDUFADs=", }; // Render the MAX_BALLS-wide ball track. When `click` ({stage,skill}) is given // and the skill isn't maxed, each ball is clickable: clicking ball K sets the // skill to level K (clicking the current top ball drops to K-1) — no steppers. function ballsHtml(level, maxed, click) { level = Math.max(0, Math.min(MAX_BALLS, Math.round(level))); var filled = maxed ? BALL.r : BALL.b; var out = ""; for (var k = 1; k <= MAX_BALLS; k++) { var on = k <= level; var style = "margin:0 1px;vertical-align:middle;" + (on ? "" : "opacity:.16;") + (click ? "cursor:pointer;" : ""); var attrs = click ? " class='mzc-ball' data-stage='" + click.stage + "' data-skill=\"" + click.skill + "\" data-k='" + k + "'" : ""; out += "<img" + attrs + " src='" + (on ? filled : BALL.b) + "' style='" + style + "'>"; } return out; } // ---- DOM parse ----------------------------------------------------------- function parseContainer(root) { var $r = $(root); // Each skill ball is <img class="skill" alt="Velocidad: 9" src=".../wlevel_9.gif"> // — name AND level live in the alt, which is far more robust than pairing // separate name spans. (Verified against live /?p=players + market HTML.) var $imgs = $r.find("img.skill"); if ($imgs.length === 0) return null; // Maxed set: van tags each maxed skill's value with a `.maxed` span. Walk // FROM each .maxed span to its row's skill name (verified live — walking the // other way, from img.skill, misses it because the span is in a sibling cell). var maxedNames = {}; $r.find(".maxed").each(function () { var a = $(this).closest("tr,div,li").find("img.skill").attr("alt") || ""; var nm0 = (a.match(/^(.+?):/) || [])[1]; if (nm0) maxedNames[nm0.trim()] = true; }); var byName = {}; // dedupe: MZ renders a responsive duplicate set per card $imgs.each(function () { var alt = ($(this).attr("alt") || "").trim(); var m = alt.match(/^(.+?):\s*(\d+)\s*$/); var nm, lvl; // Skip non-trainable rows (Experience / Fitness) — not part of a rotation. if (m && /^(Experiencia|Experience|Estado f|Fitness|Form)/i.test(m[1].trim())) return; if (m) { nm = m[1].trim(); lvl = parseInt(m[2], 10); } else { var sm = ($(this).attr("src") || "").match(/(\d+)\.gif/); if (!sm) return; nm = "?" + Object.keys(byName).length; lvl = parseInt(sm[1], 10); } // Maxed if van tagged it (.maxed set above) or it's already at the cap. var maxed = !!maxedNames[nm] || lvl >= MAX_BALLS; if (!byName[nm]) byName[nm] = { name: nm, level: lvl, maxed: maxed }; else { if (lvl > byName[nm].level) byName[nm].level = lvl; byName[nm].maxed = byName[nm].maxed || maxed; } }); var skills = Object.keys(byName).map(function (k) { return byName[k]; }); if (skills.length === 0) return null; var html = $r.html() || ""; var pidM = html.match(/player_id_(\d+)/) || ($r.find("a[href*='pid=']").attr("href") || "").match(/pid=(\d+)/); var pid = pidM ? pidM[1] : null; var name = ($r.find(".player_name, a.player_link, .name_with_team, h2, strong").first().text() || "Jugador").trim().slice(0, 40); // Age markup is "Edad: <strong>29</strong>" — allow tags/space between label and number. var ageM = ($r.text() || "").match(/(?:Edad|Age)[^0-9]{0,15}(\d{1,2})/i); var age = ageM ? parseInt(ageM[1], 10) : 21; return { pid: pid, name: name, age: age, skills: skills }; } // ---- projection ---------------------------------------------------------- // Calibrated, decelerating auto-estimate. NOT projectedGain() — that runs on // MZ's fine ~0-25 scale and overshoots when mapped to balls (see memory). This // gives a believable balls/season curve: slows as the level climbs + age rises. function autoBalls(level, age, years) { var lv = level; for (var s = 0; s < years; s++) { var ageFactor = Math.max(0.25, Math.min(1, (26 - (age + s)) / 8)); var gain = Math.max(0, Math.min(2, Math.round((MAX_BALLS - lv) * 0.3 * ageFactor))); lv = Math.min(MAX_BALLS, lv + gain); } return lv; } // ---- persistence --------------------------------------------------------- function loadPlan(pid) { try { var v = (typeof GM_getValue === "function") ? GM_getValue("mzc_proj_" + pid) : null; return v ? JSON.parse(v) : null; } catch (e) { return null; } } function savePlan(pid, plan) { try { if (typeof GM_setValue === "function") GM_setValue("mzc_proj_" + pid, JSON.stringify(plan)); } catch (e) { /* ignore */ } } // ---- modal --------------------------------------------------------------- function openModal(player) { $("#mzc-proj-overlay").remove(); // stages: [{age, deltas:{skillName:int}, auto:bool}] var saved = player.pid ? loadPlan(player.pid) : null; var stages = (saved && saved.stages && saved.stages.length) ? saved.stages : [ { age: player.age + 1, deltas: {}, auto: false }, { age: player.age + 3, deltas: {}, auto: true }, ]; function stageSkills(idx) { var lv = {}; player.skills.forEach(function (s) { lv[s.name] = s.level; }); var prevAge = player.age; for (var i = 0; i <= idx; i++) { var st = stages[i]; player.skills.forEach(function (s) { if (s.maxed) { lv[s.name] = s.level; return; } // maxed = locked at cap, never gains var cur = lv[s.name]; if (st.auto) cur = autoBalls(cur, prevAge, Math.max(0, st.age - prevAge)); var d = st.deltas[s.name] || 0; lv[s.name] = Math.max(0, Math.min(MAX_BALLS, cur + d)); }); prevAge = st.age; } return lv; } function render() { var cols = ["<th style='text-align:left;padding:6px 8px;border-bottom:2px solid #ddd'>Habilidad</th>"]; cols.push("<th style='padding:6px 8px;border-bottom:2px solid #ddd'>Hoy · " + player.age + "a</th>"); stages.forEach(function (st, i) { cols.push("<th style='padding:6px 8px;border-bottom:2px solid #ddd'>Etapa " + (i + 1) + " · <input type='number' min='" + (player.age) + "' max='40' value='" + st.age + "' data-stage='" + i + "' class='mzc-age' style='width:42px'>a" + " <label style='font-weight:normal;font-size:11px'><input type='checkbox' class='mzc-auto' data-stage='" + i + "'" + (st.auto ? " checked" : "") + "> auto</label>" + (stages.length > 1 ? " <a href='javascript:void(0)' class='mzc-del' data-stage='" + i + "' title='quitar' style='color:#b00'>✕</a>" : "") + "</th>"); }); var head = "<tr>" + cols.join("") + "</tr>"; var projByStage = stages.map(function (_, i) { return stageSkills(i); }); var todayTot = 0, stageTot = stages.map(function () { return 0; }); var rows = player.skills.map(function (s) { todayTot += s.level; var maxTag = s.maxed ? " <span style='font-size:9px;color:#c0392b;border:1px solid #c0392b;border-radius:3px;padding:0 3px;vertical-align:middle'>máx</span>" : ""; var tds = "<td style='padding:4px 8px;white-space:nowrap'>" + s.name + maxTag + "</td>"; tds += "<td style='padding:4px 8px;white-space:nowrap'>" + ballsHtml(s.level, s.maxed) + " <b>" + s.level + "</b></td>"; stages.forEach(function (st, i) { var lv = projByStage[i][s.name]; stageTot[i] += lv; var cell; if (s.maxed) { cell = ballsHtml(lv, true) + " <b>" + lv + "</b>"; // locked at cap } else if (st.auto) { cell = ballsHtml(lv, lv >= MAX_BALLS) + " <b>" + lv + "</b> <span style='color:#888;font-size:10px'>est</span>"; } else { // Manual: balls are clickable to set the level directly. cell = ballsHtml(lv, lv >= MAX_BALLS, { stage: i, skill: s.name }) + " <b>" + lv + "</b>"; } tds += "<td style='padding:4px 8px;white-space:nowrap'>" + cell + "</td>"; }); return "<tr>" + tds + "</tr>"; }).join(""); var totRow = "<tr style='border-top:2px solid #ddd;font-weight:bold'><td style='padding:6px 8px'>Total</td><td style='padding:6px 8px'>" + todayTot + "</td>" + stageTot.map(function (t) { return "<td style='padding:6px 8px'>" + t + "</td>"; }).join("") + "</tr>"; $("#mzc-proj-body").html("<table style='border-collapse:collapse;font:13px Arial'>" + head + rows + totRow + "</table>"); } var overlay = $( "<div id='mzc-proj-overlay' style='position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99999;display:flex;align-items:flex-start;justify-content:center;overflow:auto'>" + "<div style='background:#fff;color:#222;margin:40px 12px;border-radius:10px;box-shadow:0 8px 40px rgba(0,0,0,.4);max-width:96vw'>" + "<div style='display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid #eee'>" + "<b style='font:600 15px Arial'>📈 Proyección — " + player.name + " (" + player.age + "a)</b>" + "<a href='javascript:void(0)' id='mzc-proj-close' style='font-size:20px;color:#666;text-decoration:none'>✕</a></div>" + "<div id='mzc-proj-body' style='padding:10px 14px;overflow:auto'></div>" + "<div style='padding:8px 14px;border-top:1px solid #eee;display:flex;justify-content:space-between;align-items:center'>" + "<a href='javascript:void(0)' id='mzc-proj-add' class='mzbtn buttondiv button_account' style='font-weight:bold'>+ etapa</a>" + "<span style='font-size:11px;color:#888'>Manual = exacto · auto = estimado (la velocidad de entrenamiento real no es visible)</span></div>" + "</div></div>" ); $("body").append(overlay); render(); function persist() { if (player.pid) savePlan(player.pid, { stages: stages }); } overlay.on("click", "#mzc-proj-close", function () { overlay.remove(); }); overlay.on("click", function (e) { if (e.target === overlay[0]) overlay.remove(); }); overlay.on("click", "#mzc-proj-add", function () { if (stages.length >= 4) return; var last = stages[stages.length - 1]; stages.push({ age: Math.min(40, last.age + 2), deltas: {}, auto: false }); persist(); render(); }); overlay.on("click", ".mzc-del", function () { stages.splice(parseInt($(this).data("stage"), 10), 1); persist(); render(); }); overlay.on("click", ".mzc-ball", function () { var i = parseInt($(this).data("stage"), 10); var sk = String($(this).data("skill")); var k = parseInt($(this).data("k"), 10); // Compute the level this stage carries in for sk WITHOUT its own delta, then // set a delta to hit the clicked level. Clicking the current top ball drops // one (so you can decrement by clicking), giving full 0..MAX control. var savedDelta = stages[i].deltas[sk]; delete stages[i].deltas[sk]; var base = stageSkills(i)[sk]; var cur = Math.max(0, Math.min(MAX_BALLS, base + (savedDelta || 0))); var target = (k === cur) ? k - 1 : k; var delta = target - base; if (delta !== 0) stages[i].deltas[sk] = delta; persist(); render(); }); overlay.on("change", ".mzc-age", function () { var i = parseInt($(this).data("stage"), 10); stages[i].age = parseInt($(this).val(), 10) || stages[i].age; persist(); render(); }); overlay.on("change", ".mzc-auto", function () { var i = parseInt($(this).data("stage"), 10); stages[i].auto = $(this).is(":checked"); if (stages[i].auto) stages[i].deltas = {}; // auto replaces manual deltas for that stage persist(); render(); }); } // ---- mount --------------------------------------------------------------- function mount() { if (typeof $ === "undefined") return; // jQuery not ready yet — interval retries var added = 0; $(".playerContainer").each(function () { var $c = $(this); if ($c.attr("data-mzc-proj")) return; var player = parseContainer(this); if (!player) return; $c.attr("data-mzc-proj", "1"); var $btn = $("<a href='javascript:void(0)' class='mzbtn buttondiv button_account mzc-proj-btn' title='Proyectar el futuro de este jugador' style='margin:3px'>" + "<span class='buttonClassMiddle'><span style='white-space:nowrap'>📈 Proyección</span></span><span class='buttonClassRight'> </span></a>"); $btn.on("click", function () { openModal(parseContainer($c[0]) || player); }); // Native button containers differ between football and hockey. Keep our // control in its own in-flow row so it cannot overlap skills or artwork. var $controls = $c.children(".mzc-proj-controls").first(); if (!$controls.length) { $controls = $("<div class='mzc-proj-controls' style='clear:both;display:flex;align-items:center;justify-content:flex-start;gap:4px;margin:4px 0 0 4px;position:relative;z-index:1'></div>"); $c.append($controls); } $controls.append($btn); added++; }); if (added) console.log("[mzc-proj] mounted " + added + " Proyección button(s)"); } function sched() { [800, 1800, 3500].forEach(function (ms) { setTimeout(mount, ms); }); setInterval(mount, 2500); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", sched); else sched(); })(); }).call(window, __mzcPako1, __mzcDexie3, __mzcJQuery, __mzcJQuery); }); /* Pako 2 is already available from @require. */ var __mzcPako2 = globalThis.pako; /* Dexie 4 is already available from @require. */ var __mzcDexie4 = globalThis.Dexie; /* NProgress is supplied by a version-pinned @require. */ /* SheetJS is supplied by a version-pinned @require. */ /* Highcharts is supplied by a version-pinned @require. */ /* html2canvas is supplied by a version-pinned @require. */ /* spin.js is supplied by a version-pinned @require. */ // ==UserScript== // @name MazyarTools // @namespace http://tampermonkey.net/ // @version 2.4 // @description Mazyar Tools & Utilities // @copyright z7z from managerzone.com // @author z7z from managerzone.com // @license MIT // @grant GM_xmlhttpRequest // @match https://www.managerzone.com/* // @match https://test.managerzone.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com // @supportURL https://github.com/mz-ir/mazyar // ==/UserScript== // --------------------------------------- Formatter ----------------------------- function mazyarFormatBigNumber(n, sep = " ") { if (n) { const numberString = n.toString(); let formattedParts = []; for (let i = numberString.length - 1; i >= 0; i -= 3) { let part = numberString.substring(Math.max(i - 2, 0), i + 1); formattedParts.unshift(part); } return formattedParts.join(sep); } return "0"; } function mazyarFormatAverageAge(age, fractionDigits = 1) { if (age) { return age.toFixed(fractionDigits); } return "0.0"; } function mazyarFormatFileSize(b) { const s = 1024; let u = 0; while (b >= s || -b >= s) { b /= s; u++; } return (u ? b.toFixed(1) + " " : b) + " KMGTPEZY"[u] + "B"; } // -------------------------------- Parser ------------------------------------- function mazyarParseMzDate(dateString) { const [day, month, year] = dateString.split('-').map(Number); return new Date(year, month - 1, day); } function mazyarParseMzDateTime(dateTimeString) { const [date, time] = dateTimeString.split(' '); const [day, month, year] = date.split('-').map(Number); const [hours, minutes] = time.split(':').map(Number); return new Date(year, month - 1, day, hours, minutes); } function mazyarGenerateUuidV4() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0, v = c == "x" ? r : (r & 0x3) | 0x8; return v.toString(16); }); } function mazyarIsFilterHitsValid(hits) { return typeof hits === "number" && hits >= 0; } function mazyarHasDuplicates(array) { return new Set(array).size !== array.length; } function mazyarExtractSportType(doc = document) { const zone = doc.querySelector("a#shortcut_link_thezone"); if (zone) { return zone.href.indexOf("hockey") > -1 ? "hockey" : "soccer"; } return "soccer"; } function mazyarExtractClubCurrency(doc) { const players = doc.getElementById("playerAltViewTable")?.querySelectorAll("tbody tr"); if (players && players.length > 0) { const parts = players[0].querySelector("td:nth-child(3)")?.innerText.split(" "); return parts[parts.length - 1].trim(); } return ""; } function mazyarExtractNationalCurrency(doc) { // works for both domestic and foreign countries const playerNode = doc.getElementById("thePlayers_0")?.querySelector("table tbody tr:nth-child(6)"); if (playerNode) { const parts = playerNode.innerText.split(" "); return parts[parts.length - 1].trim(); } return ""; } function mazyarExtractTeamId(link) { const regex = /tid=(\d+)/; const match = regex.exec(link); return match ? match[1] : null; } function mazyarExtractPlayerIdFromProfileLink(link) { const regex = /pid=(\d+)/; const match = regex.exec(link); return match ? match[1] : null; } function mazyarExtractMatchId(link) { const regex = /mid=(\d+)/; const match = regex.exec(link); return match ? match[1] : null; } function mazyarExtractPlayerIdFromTransferMonitor(link) { const regex = /u=(\d+)/; const match = regex.exec(link); return match ? match[1] : null; } function mazyarExtractPlayerIdFromContainer(player) { return player?.querySelector("h2 span.player_id_span")?.innerText; } function mazyarIsMatchInProgress(resultText) { const scoreRegex = /\b(X|0|[1-9]\d*) - (X|0|[1-9]\d*)\b/; return !scoreRegex.test(resultText); } function mazyarIsPlayerSentToCamp(doc) { return !!doc.querySelector(`#thePlayers_0 span.player_icon_wrapper i.fa-traffic-cone.tc-status-icon:not(.tc-status-icon--disabled)`); } function mazyarExtractSkillNamesFromPlayerInfo(player) { const skills = player?.querySelectorAll("table.player_skills > tbody > tr > td > span.skill_name > span:first-child"); return [...skills].map((el) => el.innerText); } function mazyarExtractSkillsFromScoutReport(section, skillList) { const skills = section.querySelectorAll("div.flex-grow-1 ul li.blurred span"); return [...skills].map((el) => skillList.indexOf(el.innerText)); } function mazyarExtractStarsFromScoutReport(section) { return section.querySelectorAll(".stars i.lit")?.length; } function mazyarExtractResidencyDaysAndPrice(doc = document) { if (!doc) { return { days: 0, price: '' }; } const transfers = doc?.querySelector("div.baz > div > div.win_back > table.hitlist"); const history = transfers?.querySelector("tbody"); if (history?.children.length > 1) { const arrived = history?.lastChild?.querySelector("td")?.innerText; const days = Math.floor((new Date() - mazyarParseMzDate(arrived)) / 86_400_000); const price = history.lastChild?.querySelector("td:last-child")?.innerText; const currency = transfers?.querySelector("thead tr td:last-child")?.innerText?.match(/.*\((.*)\)/)?.[1]; return { days, price: price + ' ' + currency }; } return { days: -1, price: '' }; } async function mazyarExtractPlayerProfile(playerId) { const url = `https://${location.hostname}/?p=players&pid=${playerId}`; const doc = await mazyarFetchHtml(url); if (doc) { const { days, price } = mazyarExtractResidencyDaysAndPrice(doc); const camp = mazyarIsPlayerSentToCamp(doc); const skills = doc.querySelectorAll("#thePlayers_0 table.player_skills tbody tr"); let i = 0; const maxed = []; for (const skill of skills) { if (skill.querySelector(".maxed")) { maxed.push(i); } i++; } return { pid: playerId, maxed, days, price, camp, }; } return null; } function mazyarExtractPlayerNameFromContainer(player) { return player?.querySelector("span.player_name")?.innerText; } async function mazyarFetchPlayerName(playerId) { const url = `https://${location.hostname}/?p=players&pid=${playerId}`; const doc = await mazyarFetchHtml(url); if (doc) { const profile = doc.getElementById("thePlayers_0"); return mazyarExtractPlayerNameFromContainer(profile); } return null; } async function mazyarExtractPlayerScoutReport(pid, skills, sport = "soccer") { const url = `https://${location.hostname}/ajax.php?p=players&sub=scout_report&pid=${pid}&sport=${sport}`; const doc = await mazyarFetchHtml(url); if (doc) { const report = doc.querySelectorAll(".paper-content.clearfix dl dd"); if (report.length == 3) { const high = mazyarExtractStarsFromScoutReport(report[0]); const highSkills = mazyarExtractSkillsFromScoutReport(report[0], skills); const low = mazyarExtractStarsFromScoutReport(report[1]); const lowSkills = mazyarExtractSkillsFromScoutReport(report[1], skills); const trainingSpeed = mazyarExtractStarsFromScoutReport(report[2]); return { pid, H: high, HS: highSkills, L: low, LS: lowSkills, S: trainingSpeed, }; } } return null; } function mazyarExtractClubPlayersDetails(doc, currency) { const players = []; const playerNodes = doc.querySelectorAll("table#playerAltViewTable tr"); for (const playerNode of playerNodes) { const age = playerNode.querySelector("td:nth-child(5)")?.innerText.replace(/\s/g, ""); if (age) { const value = playerNode.querySelector("td:nth-child(3)")?.innerText.replaceAll(currency, "").replace(/\s/g, ""); const shirtNumber = playerNode.querySelector("td:nth-child(0)")?.innerText.replace(/\s/g, ""); const pid = playerNode.querySelector("a")?.href; players.push({ shirtNumber, age: parseInt(age, 10), value: parseInt(value, 10), id: mazyarExtractPlayerIdFromProfileLink(pid), }); } } return players; } function mazyarExtractNumberOfFlags(infoTable) { const images = infoTable.getElementsByTagName("img"); return images ? [...images].filter((img) => img.src.indexOf("/flags/") > -1).length : 0; } function mazyarIsPlayerDomestic(infoTable) { return mazyarExtractNumberOfFlags(infoTable) === 1; } function mazyarExtractNationalPlayersDetails(doc, currency) { const players = []; const playerNodes = doc.querySelectorAll("div.playerContainer"); for (const playerNode of playerNodes) { const id = mazyarExtractPlayerIdFromProfileLink(playerNode.querySelector("h2 a")?.href); const infoTable = playerNode.querySelector("div.dg_playerview_info table"); const age = infoTable.querySelector("tbody tr:nth-child(1) td strong").innerText; const selector = mazyarIsPlayerDomestic(infoTable) ? "tbody tr:nth-child(5) td span" : "tbody tr:nth-child(6) td span"; const value = infoTable.querySelector(selector)?.innerText.replaceAll(currency, "").replace(/\s/g, ""); players.push({ age: parseInt(age, 10), value: parseInt(value, 10), id, }); } return players; } function mazyarGetNumberOfPlayers(players, ageLow = 0, ageHigh = 99) { return players.filter((player) => player.age <= ageHigh && player.age >= ageLow).length; } // ----------------------------------- Fetch --------------------------------- async function mazyarFetchHtmlWithGM(url, timeout = 10) { try { const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ url, // timeout: timeout * 1000, onload: (resp) => resolve(resp), onerror: reject }); }); const parser = new DOMParser(); return parser.parseFromString(response.responseText, "text/html"); } catch (error) { console.warn(error); return null; } } async function mazyarFetchHtml(url) { return await fetch(url) .then((resp) => resp.text()) .then((content) => { const parser = new DOMParser(); return parser.parseFromString(content, "text/html"); }) .catch((error) => { console.warn(error); return null; }); } async function mazyarFetchXml(url) { return await fetch(url) .then((resp) => resp.text()) .then((content) => { const parser = new DOMParser(); return parser.parseFromString(content, "text/xml"); }) .catch((error) => { console.warn(error); return null; }); } async function mazyarFetchJson(url) { return await fetch(url) .then((resp) => resp.json()) .catch((error) => { console.warn(error); return null; }); } function mazyarGetSquadSummaryUrl(tid) { return `https://${location.hostname}/?p=players&sub=alt&tid=${tid}`; } function mazyarExtractSquadSummaryDetails(players, sport = "soccer") { if (!players) { return []; } const rows = []; if (sport === "hockey") { { const all = mazyarFilterPlayers(players); const top21 = mazyarFilterPlayers(players, 21); rows.push({ title: "All", count: players.length, all: all.values, allAge: all.avgAge, top21: top21.values, top21Age: top21.avgAge, }); } { const all = mazyarFilterPlayers(players, 0, 0, 23); const top21 = mazyarFilterPlayers(players, 21, 0, 23); rows.push({ title: "U23", count: mazyarGetNumberOfPlayers(players, 0, 23), all: all.values, allAge: all.avgAge, top21: top21.values, top21Age: top21.avgAge, }); } { const all = mazyarFilterPlayers(players, 0, 0, 21); const top21 = mazyarFilterPlayers(players, 21, 0, 21); rows.push({ title: "U21", count: mazyarGetNumberOfPlayers(players, 0, 21), all: all.values, allAge: all.avgAge, top21: top21.values, top21Age: top21.avgAge, }); } { const all = mazyarFilterPlayers(players, 0, 0, 18); const top21 = mazyarFilterPlayers(players, 21, 0, 18); rows.push({ title: "U18", count: mazyarGetNumberOfPlayers(players, 0, 18), all: all.values, allAge: all.avgAge, top21: top21.values, top21Age: top21.avgAge, }); } } else { { const all = mazyarFilterPlayers(players); const top16 = mazyarFilterPlayers(players, 16); const top11 = mazyarFilterPlayers(players, 11); rows.push({ title: "All", count: players.length, all: all.values, allAge: all.avgAge, top16: top16.values, top16Age: top16.avgAge, top11: top11.values, top11Age: top11.avgAge, }); } { const all = mazyarFilterPlayers(players, 0, 0, 23); const top16 = mazyarFilterPlayers(players, 16, 0, 23); const top11 = mazyarFilterPlayers(players, 11, 0, 23); rows.push({ title: "U23", count: mazyarGetNumberOfPlayers(players, 0, 23), all: all.values, allAge: all.avgAge, top16: top16.values, top16Age: top16.avgAge, top11: top11.values, top11Age: top11.avgAge, }); } { const all = mazyarFilterPlayers(players, 0, 0, 21); const top16 = mazyarFilterPlayers(players, 16, 0, 21); const top11 = mazyarFilterPlayers(players, 11, 0, 21); rows.push({ title: "U21", count: mazyarGetNumberOfPlayers(players, 0, 21), all: all.values, allAge: all.avgAge, top16: top16.values, top16Age: top16.avgAge, top11: top11.values, top11Age: top11.avgAge, }); } { const all = mazyarFilterPlayers(players, 0, 0, 18); const top16 = mazyarFilterPlayers(players, 16, 0, 18); const top11 = mazyarFilterPlayers(players, 11, 0, 18); rows.push({ title: "U18", count: mazyarGetNumberOfPlayers(players, 0, 18), all: all.values, allAge: all.avgAge, top16: top16.values, top16Age: top16.avgAge, top11: top11.values, top11Age: top11.avgAge, }); } } return rows; } function mazyarFilterPlayers(players, count = 0, ageLow = 0, ageHigh = 99) { if (players.length === 0) { return { values: 0, avgAge: 0.0 }; } const n = count === 0 ? players.length : count; const filtered = players .filter((player) => player.age <= ageHigh && player.age >= ageLow) .sort((a, b) => b.value - a.value) .slice(0, n); if (filtered.length === 0) { return { values: 0, avgAge: 0.0 }; } const values = filtered.map((player) => player.value).reduce((a, b) => a + b, 0); const avgAge = filtered.map((player) => player.age).reduce((a, b) => a + b, 0) / filtered.length; return { values, avgAge }; } async function mazyarFetchNationalPlayersAndCurrency(tid, sport) { const url = `https://${location.hostname}/ajax.php?p=nationalTeams&sub=players&ntid=${tid}&sport=${sport}`; const doc = await mazyarFetchHtml(url); if (doc) { const currency = mazyarExtractNationalCurrency(doc); const players = mazyarExtractNationalPlayersDetails(doc, currency); return { players, currency }; } return { players: [], currency: '' }; } async function mazyarFetchClubPlayersAndCurrency(tid) { const url = mazyarGetSquadSummaryUrl(tid); const doc = await mazyarFetchHtml(url); if (doc) { const currency = mazyarExtractClubCurrency(doc); const players = mazyarExtractClubPlayersDetails(doc, currency); return { players, currency }; } return { players: [], currency: '' }; } async function mazyarFetchPlayersAndCurrency(tid, sport) { const url = mazyarGetSquadSummaryUrl(tid); const isNational = await fetch(url, { method: "HEAD" }) .then((resp) => (resp.url.search("p=national_teams") > -1)); return isNational ? await mazyarFetchNationalPlayersAndCurrency(tid, sport) : await mazyarFetchClubPlayersAndCurrency(tid); } function mazyarExtractClubTopPlyers(doc) { const currency = mazyarExtractClubCurrency(doc); const players = mazyarExtractClubPlayersDetails(doc, currency); const sport = mazyarExtractSportType(doc); const count = sport === "soccer" ? 11 : 21; return players ? mazyarFilterPlayers(players, count) : { values: 0, avgAge: 0 }; } async function mazyarExtractPlayersProfileDetails(teamId) { if (!teamId) { return null; } const url = `https://${location.hostname}/?p=players&tid=${teamId}`; const doc = await mazyarFetchHtml(url); if (doc) { const players = doc.getElementById("players_container")?.querySelectorAll("div.playerContainer"); const info = {}; for (const player of players) { const playerId = player.querySelector("span.player_id_span")?.innerText; const inMarket = [...player.querySelectorAll("a")].find((el) => el.href?.indexOf("p=transfer&sub") > -1); info[playerId] = { detail: player, shared: !!player.querySelector("i.special_player.fa-share-alt"), market: !!inMarket, marketLink: inMarket?.href, name: player.querySelector("span.player_name")?.innerText, age: player.querySelector(".dg_playerview_info tbody tr:nth-child(1) td:nth-child(1) strong")?.innerText, } } return info; } return null; } // -------------------------------------------- Icons ------------------------------------------ function mazyarStartSpinning(element) { element.classList.add("fa-spin"); } function mazyarStopSpinning(element) { element.classList.remove("fa-spin"); } function mazyarCreateIconFromFontAwesomeClass(classes = [], title = "") { const icon = document.createElement("i"); icon.classList.add(...classes); icon.setAttribute("aria-hidden", "true"); icon.style.cursor = "pointer"; if (title) { icon.title = title; } return icon; } function mazyarCreateToggleIcon(title, isOn) { return mazyarCreateIconFromFontAwesomeClass(["fas", isOn ? "fa-toggle-on" : "fa-toggle-off"], title); } function mazyarCreateMoveIcon(title) { return mazyarCreateIconFromFontAwesomeClass(["fa-solid", "fa-up-down-left-right"], title); } function mazyarCreateSharedIcon(title) { return mazyarCreateIconFromFontAwesomeClass(["fa", "fa-share-alt"], title); } function mazyarCreateMarketIcon(title) { return mazyarCreateIconFromFontAwesomeClass(["fa", "fa-legal"], title); } function mazyarCreateCogIcon(title = "") { return mazyarCreateIconFromFontAwesomeClass(["fa", "fa-cog"], title); } function mazyarCreateCommentIcon(title = "", style = null) { const icon = mazyarCreateIconFromFontAwesomeClass(["fa-solid", "fa-comment"], title); icon.style.cursor = "pointer"; if (style?.fontSize) { icon.style.fontSize = style.fontSize; } if (style?.margin) { icon.style.margin = style.margin; } return icon } function mazyarCreateSearchIcon(title = "") { return mazyarCreateIconFromFontAwesomeClass(["fa", "fa-search"], title); } function mazyarCreateNoteIcon(title = "") { return mazyarCreateIconFromFontAwesomeClass(["fa-solid", "fa-note-sticky"], title); } function mazyarCreateRefreshIcon(title = "") { return mazyarCreateIconFromFontAwesomeClass(["fa", "fa-refresh"], title); } function mazyarCreateLegalIcon(title = "") { return mazyarCreateIconFromFontAwesomeClass(["fa", "fa-legal"], title); } function mazyarCreateSignalIcon(title = "") { return mazyarCreateIconFromFontAwesomeClass(["fa-solid", "fa-signal-stream"], title); } function mazyarCreateLoadingIcon(title = "") { const icon = mazyarCreateIconFromFontAwesomeClass(["fa", "fa-spinner", "fa-spin"], title); icon.style.cursor = "unset"; return icon; } function mazyarCreateLoadingIcon2(title = "") { const icon = mazyarCreateIconFromFontAwesomeClass(["fa-solid", "fa-loader", "fa-pulse", "fa-fw"], title); icon.style.cursor = "unset"; return icon; } function mazyarCreateTrashIcon(title = "", style = { fontSize: "1rem", margin: "unset" }) { const icon = mazyarCreateIconFromFontAwesomeClass(["fa-solid", "fa-trash", "discard-icon"], title); icon.style.cursor = "pointer"; if (style?.fontSize) { icon.style.fontSize = style.fontSize; } if (style?.margin) { icon.style.margin = style.margin; } return icon; } function mazyarCreateAddToDeadlineIcon(title, color) { const icon = mazyarCreateLegalIcon(); icon.style.verticalAlign = "unset"; icon.style.borderRadius = "50%"; icon.style.border = "solid 1px"; icon.style.padding = "3px"; const span = document.createElement("span"); span.style.color = color; span.classList.add("floatRight"); if (title) { span.title = title; } span.appendChild(icon); return span; } function mazyarCreateHideFromTransferIcon(title) { const icon = mazyarCreateTrashIcon(title, { fontSize: "0.9rem" }); icon.style.verticalAlign = "unset"; icon.style.padding = "3px"; const span = document.createElement("span"); span.classList.add("floatRight"); if (title) { span.title = title; } span.appendChild(icon); return span; } function mazyarCreateCommentIconForTransferResults(title) { const icon = mazyarCreateCommentIcon(title, { fontSize: "1.1rem" }); icon.style.verticalAlign = "unset"; icon.style.padding = "3px"; const span = document.createElement("span"); span.classList.add("floatRight"); if (title) { span.title = title; } span.appendChild(icon); return span; } // -------------------------------- DOM Utils ------------------------------ async function mazyarGetTransferHistory(url) { const doc = await mazyarFetchHtml(url); if (doc) { const table = doc.querySelector("#transfer-history"); const tbody = table.querySelector("tbody"); tbody.querySelectorAll("tr:not(.bought, .sold)")?.forEach((child) => { tbody.removeChild(child); }); return table; } return null; } async function mazyarGetTableTransferHistories(table) { const teams = table.querySelectorAll("tbody tr.responsive-hide"); const jobs = []; for (const team of teams) { const teamLink = team.querySelector("td:nth-child(2) a:last-child"); const tid = mazyarExtractTeamId(teamLink?.href); const url = `https://${location.hostname}/?p=transfer_history&tid=${tid}`; jobs.push(mazyarGetTransferHistory(url).then((history) => { if (history) { const name = document.createElement("td"); name.innerHTML = `For Team: <a style="text-decoration: none !important;" href="${url}">${teamLink?.innerText}</a>`; name.colSpan = 5; // name.style.backgroundColor = "khaki"; name.style.fontWeight = "bold"; name.style.padding = "10px 5px 5px"; const tr = document.createElement("tr"); tr.classList.add("mazyar-history-name", "hitlist-compact-list-column"); tr.style.border = "2px solid darkgray"; tr.appendChild(name); history.querySelector("tbody").prepend(tr); return history; } return null; })); } return (await Promise.all(jobs)).filter((h) => !!h); } function mazyarFilterTransferHistory(history, weeks = 2) { history.filterResults = 0; const players = history?.querySelectorAll("tbody tr:not(.mazyar-history-name)"); players?.forEach((child) => { const date = child.querySelector("td.hitlist-compact-list-column")?.firstChild?.nodeValue?.trim(); if (Date.now() - mazyarParseMzDate(date) >= weeks * 7 * 24 * 60 * 60 * 1000) { child.style.display = 'none'; } else { child.style.display = "table-row"; history.filterResults += 1; } }); } function mazyarRemoveOldTransferHistory(history, weeks = 4) { const players = history?.querySelectorAll("tbody tr:not(.mazyar-history-name)"); players?.forEach((child) => { const date = child.querySelector("td.hitlist-compact-list-column")?.firstChild?.nodeValue?.trim(); if (Date.now() - mazyarParseMzDate(date) >= weeks * 7 * 24 * 60 * 60 * 1000) { child.parentNode.removeChild(child); } }); } function mazyarMakeElementDraggable(element, dragHandleElement, dragEndCallback = null) { let deltaX = 0, deltaY = 0, lastX = 0, lastY = 0; dragHandleElement.style.cursor = "move"; dragHandleElement.onmousedown = dragMouseDown; function dragMouseDown(e) { e = e || window.event; e.preventDefault(); // get the mouse cursor position at startup: lastX = e.clientX; lastY = e.clientY; document.onmouseup = closeDragElement; // call a function whenever the cursor moves: document.onmousemove = elementDrag; } function elementDrag(e) { e = e || window.event; e.preventDefault(); // calculate the new cursor position: deltaX = lastX - e.clientX; deltaY = lastY - e.clientY; lastX = e.clientX; lastY = e.clientY; // set the element's new position: let newTop = Math.max(0, element.offsetTop - deltaY); let newLeft = Math.max(0, element.offsetLeft - deltaX); const { right, bottom, width, height } = element.getBoundingClientRect(); if (right > window.innerWidth) { newLeft = window.innerWidth - width; } if (bottom > window.innerHeight) { newTop = window.innerHeight - height; } element.style.top = newTop + "px"; element.style.left = newLeft + "px"; } function closeDragElement() { // stop moving when mouse button is released: document.onmouseup = null; document.onmousemove = null; if (dragEndCallback) { dragEndCallback(); } } } function mazyarColorizeMaxedSkills(player, maxed = []) { if (maxed) { const playerSkills = player.querySelectorAll("table.player_skills tr td.skillval span"); for (const skill of maxed) { playerSkills[skill]?.classList.add("maxed"); } } } function mazyarColorizeSkills(player, report = { H: 4, L: 2, HS: [0, 1], LS: [2, 3] }) { if (report) { const playerSkills = player?.querySelectorAll("table.player_skills tr td:nth-child(1)"); playerSkills[report.HS[0]]?.classList.add("mazyar-scout-h", `mazyar-scout-${report.H}`); playerSkills[report.HS[1]]?.classList.add("mazyar-scout-h", `mazyar-scout-${report.H}`); playerSkills[report.LS[0]]?.classList.add(`mazyar-scout-${report.L}`); playerSkills[report.LS[1]]?.classList.add(`mazyar-scout-${report.L}`); } } function mazyarGetMzButtonColorClass(color) { if (color) { if (color === "red") { return "button_red"; } else if (color === "blue") { return "button_blue"; } else if (color === "grey") { return "buttondiv_disabled"; } } // green or other values return "button_account"; } function mazyarCreateMzStyledButton(title, color = "", floatDirection = null) { const div = document.createElement("div"); div.style.margin = "auto 0.3rem"; if (floatDirection) { // floatDirection: floatRight, floatLeft div.classList.add(floatDirection); } const button = document.createElement("div"); button.classList.add("mzbtn", "buttondiv", mazyarGetMzButtonColorClass(color)); button.innerHTML = `<span class="buttonClassMiddle"><span style="white-space: nowrap">${title}</span></span><span class="buttonClassRight"> </span>`; div.appendChild(button); return div; } function mazyarCreateSettingsSectionButton(html = "", style = { backgroundColor: null }) { const button = document.createElement("button"); button.innerHTML = html; button.classList.add("mazyar-settings-section-button"); if (style?.backgroundColor) { button.style.backgroundColor = style.backgroundColor; } return button; } function mazyarCreateUpdateTip(version, url) { const div = document.createElement("div"); div.classList.add("mazyar-update-tip"); div.innerHTML = ` <span><b>Update Available!</b></span><br /> <button><a href="${url}" target="_blank">Install v${version}</a></button>`; return div; } function mazyarCreateMzStyledCloseButton(closeCallback) { const close = document.createElement("div"); close.classList.add("mazyar-cross-close-button"); close.innerHTML = ` <span class="fa-stack fa-lg"> <i class="fa fa-circle fa-stack-2x fa-inverse"></i> <i class="fa fa-close fa-stack-1x"></i> </span>`; if (closeCallback) { close.addEventListener("click", closeCallback); } return close; } function mazyarCreateMzStyledModalHeader(text = "", closeCallback = null) { const header = document.createElement("div"); header.classList.add("mazyar-flexbox-row", "mazyar-modal-header"); const title = document.createElement("span"); title.innerText = text; title.style.flexGrow = "1"; title.style.fontWeight = "bold"; title.style.fontSize = "larger"; title.style.padding = "5px"; header.appendChild(title); if (closeCallback) { const close = mazyarCreateMzStyledCloseButton(closeCallback); header.appendChild(close); } return header; } function mazyarCreateSuggestionList(items) { const datalist = document.createElement("datalist"); datalist.id = mazyarGenerateUuidV4(); for (const item of items) { const option = document.createElement("option"); option.value = item.toString(); datalist.appendChild(option); } return datalist; } function mazyarCreateMenuTextInput(title = "input", placeholder = "example", datalistId = "") { const div = document.createElement("div"); div.classList.add("mazyar-flexbox-row"); div.style.justifyItems = "space-between"; div.innerHTML = ` <label style="margin: 0.5rem; font-weight: bold;">${title}: </label> <input list="${datalistId}" style="margin: 0.5rem;" type="text" value="" placeholder="${placeholder}">`; return div; } function mazyarCreateSubMenuTextInput(title = "input", placeholder = "example", initialValue = 0, style = { margin: "0.1rem 2.2rem", inputSize: "5px" }) { const div = document.createElement("div"); div.classList.add("mazyar-flexbox-row"); div.style.justifyItems = "space-between"; div.style.margin = style?.margin ?? "0.1rem 2.2rem"; div.innerHTML = ` <label style="margin-left: 0.5rem;">${title}: </label> <input style="margin-left: 0.5rem;" type="text" size="${style?.inputSize ?? "5px"}" placeholder="${placeholder}", value="${initialValue}">`; return div; } function mazyarCreateMenuCheckBox( label, initialValue = true, style = { alignSelf: "flex-start", margin: "0.3rem 0.7rem", } ) { const id = mazyarGenerateUuidV4(); const div = document.createElement("div"); div.style.alignSelf = style?.alignSelf ?? "flex-start"; div.style.margin = style?.margin ?? "0.3rem 0.7rem"; const checkbox = document.createElement("input"); checkbox.id = id; checkbox.type = "checkbox"; checkbox.checked = initialValue; const labelElement = document.createElement("label"); labelElement.htmlFor = id; labelElement.innerHTML = label; div.appendChild(checkbox); div.appendChild(labelElement); return div; } function mazyarCreateMenuGroup(title = "") { const group = document.createElement("div"); group.classList.add("mazyar-flexbox-column"); group.style.alignSelf = "flex-start"; group.style.alignItems = "flex-start"; group.style.margin = "0.2rem 0.6rem"; const header = document.createElement("h4"); header.innerText = title; header.style.margin = "0.3rem 0rem"; group.appendChild(header); return group; } function mazyarAppendOptionList(parent, options, selected) { // options = a object full of 'key: {value, label}' for (const key in options) { if (options.hasOwnProperty(key)) { const child = document.createElement("option"); child.value = options[key].value; child.innerText = options[key].label; if (child.value === selected) { child.selected = true; } parent.appendChild(child); } } } function mazyarCreateDropDownMenu(label, options, initialValue) { // options = a object full of 'key: {value, label}' // initialValue = one of the options.value const div = document.createElement("div"); const labelElement = document.createElement("label"); const dropdown = document.createElement("select"); div.style.alignSelf = "flex-start"; div.style.margin = "0.3rem 0.7rem"; labelElement.innerText = label; labelElement.style.paddingRight = "0.5rem"; mazyarAppendOptionList(dropdown, options, initialValue); div.appendChild(labelElement); div.appendChild(dropdown); return div; } function mazyarCreateDeleteAllFiltersButton(title = "Delete", clickCallback = null) { const div = document.createElement("div"); div.classList.add("mazyar-flexbox-row"); div.style.width = "100%"; const button = document.createElement("button"); button.classList.add("mazyar-button"); const icon = mazyarCreateTrashIcon(null, { fontSize: "0.9rem", margin: "1px 3px" }); const text = document.createElement("span"); text.innerText = title; text.style.fontWeight = "bold"; button.appendChild(icon); button.appendChild(text); div.append(button); button.style.margin = "4px"; button.style.padding = "3px"; if (clickCallback) { button.addEventListener("click", clickCallback); } return div; } function mazyarCreateTableHeaderForFiltersView() { const tr = document.createElement("tr"); const name = document.createElement("th"); name.classList.add("header"); name.innerText = "Name"; name.title = "Filter name"; name.style.textAlign = "left"; name.style.textDecoration = "none"; name.style.width = "11rem"; const totalHits = document.createElement("th"); totalHits.classList.add("header"); totalHits.innerText = "Total"; totalHits.title = "Total hits founds for this filter"; totalHits.style.textAlign = "center"; totalHits.style.textDecoration = "none"; const scoutHits = document.createElement("th"); scoutHits.classList.add("header"); scoutHits.innerText = "Scout"; scoutHits.title = "Hits found after applying scout filters"; scoutHits.style.textAlign = "center"; scoutHits.style.textDecoration = "none"; const tools = document.createElement("th"); tools.classList.add("header"); tools.innerHTML = " "; tools.style.textAlign = "center"; tools.style.textDecoration = "none"; tr.appendChild(tools); tr.appendChild(name); tr.appendChild(totalHits); tr.appendChild(scoutHits); const thead = document.createElement("thead"); thead.appendChild(tr); return thead; } function mazyarCreateTableHeaderForCommentsView() { const tr = document.createElement("tr"); const pid = document.createElement("th"); pid.classList.add("header"); pid.innerText = "ID"; pid.title = "Player ID"; pid.style.textAlign = "left"; pid.style.textDecoration = "none"; const name = document.createElement("th"); name.classList.add("header"); name.innerText = "Name"; name.title = "Player Name"; name.style.textAlign = "left"; name.style.textDecoration = "none"; name.style.minWidth = "7rem"; const del = document.createElement("th"); del.classList.add("header"); del.innerHTML = " "; del.style.textAlign = "center"; del.style.textDecoration = "none"; const view = document.createElement("th"); view.classList.add("header"); view.innerHTML = " "; view.style.textAlign = "center"; view.style.textDecoration = "none"; tr.appendChild(del); tr.appendChild(pid); tr.appendChild(name); tr.appendChild(view); const thead = document.createElement("thead"); thead.appendChild(tr); return thead; } function mazyarCreateToolbar() { const toolbar = document.createElement("div"); const logo = document.createElement("span"); const menu = mazyarCreateCogIcon("Settings"); const note = mazyarCreateNoteIcon("Notebook"); const comments = mazyarCreateCommentIcon("Comments"); const live = mazyarCreateSignalIcon("In Progress Results"); const separator = document.createElement("span"); const transfer = document.createElement("div"); const transferIcon = mazyarCreateSearchIcon("Transfer"); const transferCount = document.createElement("span"); toolbar.id = "mazyar-toolbar-overlay"; toolbar.classList.add("mazyar-flexbox-column"); logo.innerText = "MZY"; logo.style.fontSize = "0.6rem"; logo.style.fontWeight = "bold"; logo.style.margin = "2px"; logo.style.padding = "1px"; menu.style.fontSize = "large"; note.style.fontSize = "large"; note.style.marginTop = "5px"; comments.style.fontSize = "large"; comments.style.marginTop = "5px"; live.style.fontSize = "large"; live.style.marginTop = "5px"; live.id = "mazyar-in-progress-icon"; transferIcon.style.fontSize = "large"; separator.innerText = "-------"; separator.style.textAlign = "center"; separator.style.fontSize = "0.6rem"; separator.style.fontWeight = "bolder"; separator.style.margin = "0"; separator.style.padding = "0"; transfer.classList.add("mazyar-flexbox-column"); transfer.style.cursor = "pointer"; transferCount.id = "mazyar-transfer-filter-hits"; transferCount.innerText = "0"; transferCount.style.fontSize = "0.6rem"; transferCount.style.fontWeight = "bold"; transferCount.style.margin = "1px 0"; transferCount.style.padding = "1px"; transfer.appendChild(transferIcon); transfer.appendChild(transferCount); toolbar.appendChild(logo); toolbar.appendChild(menu); toolbar.appendChild(note); toolbar.appendChild(comments); toolbar.appendChild(live); toolbar.appendChild(separator); toolbar.appendChild(transfer); return { toolbar, menu, transfer, note, comments, live }; } function mazyarCreateFiltersOverview(filters, label, labelColor = "black") { const div = document.createElement("div"); div.classList.add("mazyar-flexbox-column"); div.style.minWidth = "250px"; div.style.marginBottom = "20px"; div.style.alignItems = "flex-start"; const header = document.createElement("div"); header.innerText = label; header.style.color = labelColor; header.style.fontWeight = "bolder"; header.style.padding = "5px"; header.style.alignSelf = "center"; div.appendChild(header); const rows = document.createElement("div"); rows.classList.add("mazyar-flexbox-column"); rows.style.justifyContent = "left"; rows.style.margin = "8px"; div.appendChild(rows); if (filters.soccer.length > 0 || filters.hockey.length > 0) { if (filters.soccer.length > 0) { const title = document.createElement("div"); title.classList.add("mazyar-import-sport-title"); title.innerHTML = "<b>Soccer:</b>"; rows.appendChild(title); for (const filter of filters.soccer) { const row = document.createElement("div"); row.classList.add("mazyar-import-filter-row"); row.innerHTML = ` <span> name: <b style="color: ${labelColor}">${filter.name}</b> </span>`; rows.appendChild(row); } } if (filters.hockey.length > 0) { const title = document.createElement("div"); title.classList.add("mazyar-import-sport-title"); title.innerHTML = "<b>Hockey:</b>"; rows.appendChild(title); for (const filter of filters.hockey) { const row = document.createElement("div"); row.classList.add("mazyar-import-filter-row"); row.innerHTML = ` <span> name: <b style="color: ${labelColor}">${filter.name}</b> </span>`; rows.appendChild(row); } } } else { rows.appendChild(document.createTextNode("No Filters")); } return div; } // -------------------------------- Monitor ------------------------------ function mazyarCreateSectionSeparatorForMonitor() { const tr = document.createElement("tr"); tr.style.height = "10px"; tr.innerHTML = '<td></td>'; return tr; } function mazyarCreateRowSeparatorForMonitor(color) { const tr = document.createElement("tr"); tr.classList.add("mazyar-monitor-player-row"); tr.style.height = "1px"; tr.style.backgroundColor = color; tr.innerHTML = '<td></td>'; return tr; } function monitorAddRowSeparator() { return [ mazyarCreateRowSeparatorForMonitor("#999999"), mazyarCreateRowSeparatorForMonitor("#FFFFFF") ]; } function mazyarCreateSectionForMonitor(title, id) { const tr = document.createElement("tr"); tr.innerHTML = ` <td> <div id="${id}"> <table width="100%" cellpadding="0" cellspacing="0"> <tbody> <tr> <td style="background-image: url(img/subheader_right.gif);"> <table border="0" cellpadding="0" cellspacing="0" width="100%"> <tbody> <tr> <td class="subheader" valign="bottom">${title}</td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </div> </td>`; return tr; } function mazyarClearPlayerRowsInMonitor(tbody) { const players = tbody.querySelectorAll(".mazyar-monitor-player-row"); for (const player of players) { tbody.removeChild(player); } } async function mazyarFetchPlayerMarketDetail(pid, sport = "soccer") { const url = `https://${location.hostname}/ajax.php?p=transfer&sub=transfer-search&sport=${sport}&u=${pid}`; const result = await mazyarFetchJson(url); if (result) { if (result.totalHits > 0) { const parser = new DOMParser(); const playerDiv = parser.parseFromString(result?.players, "text/html").body.firstChild; const deadline = playerDiv.querySelector(".transfer-control-area div.box_dark:nth-child(1) table:nth-child(1) tr:nth-child(3) strong")?.innerText; const fee = playerDiv.querySelector(".transfer-control-area > div > div:nth-child(1) > table > tbody > tr:last-child strong")?.innerText; const latestBid = playerDiv.querySelector(".transfer-control-area > div > div:nth-child(2) > table > tbody > tr:last-child strong")?.innerText; const flag = playerDiv.querySelector(`img[src*="/flags/"]`)?.src; const player = { name: playerDiv.querySelector(".player_name")?.innerText, pid, deadline: 1 + Math.ceil((mazyarParseMzDateTime(deadline.trim()) - new Date()) / 60_000), deadlineFull: deadline.trim(), latestBid, fee, source: "mzy", flag, }; return { player, remove: false }; } return { player: null, remove: true }; } return { player: null, remove: false }; } async function mazyarFetchTrainingCampDetail() { const url = `https://${location.hostname}/?p=training_camp`; const doc = await mazyarFetchHtml(url); if (doc) { const players = doc.querySelectorAll("#training_camp_players_container div.tc_table_container"); const detail = {}; for (const player of players) { const pid = mazyarExtractPlayerIdFromProfileLink(player.querySelector(".player_link")?.href); const daysFull = player.querySelector(".tc_table_duration > div:nth-child(2)")?.innerText; detail[pid] = { days: daysFull?.split(": ")?.[1], } } return detail; } return null; } function mazyarCreatePlayerRowForMonitor( player = { pid: "", name: "", deadline: 0, deadlineFull: "", latestBid: "", flag: "" }, timeout = 0) { const tr = document.createElement("tr"); tr.classList.add("mazyar-monitor-player-row"); if (player.deadline <= timeout) { tr.classList.add("mazyar-deadline-monitor-throb"); } tr.innerHTML = ` <td valign="top" style="" width="100%"> <table width="100%" border="0"> <tbody> <tr style="height: 25px;"> <td colspan="2"> <table cellpadding="0" cellspacing="0" width="100%" border="0"> <tbody> <tr> <td width="220"> <table> <tbody> <tr> <td><img src="${player.flag}"></td> <td><a target="_blank", href="/?p=transfer&sub=players&u=${player.pid}">${player.name}</a></td> <td></td> </tr> </tbody> </table> </td> <td> <table class="deadline-table"> <tbody> <tr> <td><img src="img/icon_deadline.gif" width="13" height="15"></td> <td>${player.deadlineFull}</td> </tr> </tbody> </table> </td> <td align="right"> <table border="0"> <tbody> <tr> <td>Latest bid:</td> <td align="right" style="font-size: 11px; font-weight: bold;">${player.latestBid}</td> <td> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> </td>`; return tr; } // -------------------------------- Tools ------------------------------ function mazyarConvertFilterArrayToFilterObject(filters) { return filters?.reduce((filter, { id, name, params, scout, interval }) => { filter[name] = { id, name, params, scout, interval }; return filter; }, {}); } function mazyarMergeFilters(a = [], b = []) { // 'b' members could replace 'a' members const aObj = mazyarConvertFilterArrayToFilterObject(a); const bObj = mazyarConvertFilterArrayToFilterObject(b); return Object.values({ ...aObj, ...bObj }); } /* mzcopilot:module:[email protected] */ window.__mzcMega.run("mazyar", "4.7", function () { (function (Dexie, $, jQuery) { (async function () { "use strict"; let mazyar = null; /* ********************** Constants ******************************** */ const CURRENT_VERSION = GM_info.script.version; const UPDATE_CHECK_INTERVAL_MINUTES = 60; const DEADLINE_INTERVAL_SECONDS = 30; const MAZYAR_CHANGELOG = { "4.7": [ "<b>[new]</b> add option to disable a transfer filter.", ], "4.6": [ "<b>[fix]</b> fixed filters with scouted skills enabled after new managerzone transfer market update.", ], "4.5": [ "<b>[new]</b> add comment icon to toolbar to open a modal to display all the comments.", "<b>[new]</b> add comment icon to player profile in transfer market.", ], "4.4": [ "<b>[fix]</b> fixed transfer view after new managerzone transfer market update.", ], "4.3": [ "<b>[fix]</b> wrong update indicator.", ], "4.2": [ "<b>[new]</b> Mazyar will check for updates every hour." + " If a new update is available, the cog icon in the MZY Toolbar will turn green and start rotating." + " Click on the icon to view the update.", ], "4.1": [ "<b>[fix]</b> MZY Transfer Filters: after delete last filter, table must be removed too.", "<b>[remove]</b> Transfer Market: 'MZY Filters' button is removed from transfer market. Use magnifying glass icon on MZY Toolbar.", ], "4.0": [ "<b>[new]</b> Import & Export Filters: use calendar to export filters (as a note).", "<b>[fix]</b> style of Mazyar modals.", "<b>[fix]</b> location of Transfer List & History in friendly leagues.", ], "3.14": [ "<b>[improve]</b> Table Transfer History: add a drop-down menu to filter the results to a narrower period", ], "3.13": [ "<b>[new]</b> Table Transfer History: add teams' last month transfer history to the standing tables", ], "3.12": [ "<b>[fix]</b> Transfer: training camp indicator in main host (www.managerzone.com)", ], "3.11": [ "<b>[new]</b> Training Report: add player's days left in the camp", ], "3.10": [ "<b>[fix]</b> Table Transfer List: fix undefined age", ], "3.9": [ "<b>[new]</b> MZY Modal: add close button in title", "<b>[improve]</b> MZY Settings: update sections' styles", "<b>[improve]</b> Table's Transfer List: add team name, player age, deadline, fee and latest bid.", ], "3.8": [ "<b>[fix]</b> fixture: full names", ], "3.7": [ `<b>[new]</b> MZY Settings: add <i class="fa-solid fa-signal-stream" style="color: green; font-size: large;"></i> icon in MZY Toolbar to toggle 'display in progress results' settings`, `<b>[new]</b> Tables: add a section to league tables to display players that are in transfer market. You can disable it in Miscellaneous section of MZY Settings. (look for <b>Add transfer list in tables</b>`, ], "3.6": [ "<b>[fix]</b> Clash: squad values", ], "3.5": [ "<b>[fix]</b> Clash: fix crash.", ], "3.4": [ "<b>[new]</b> MZY Settings is splitted to smaller sections.", ], "3.3": [ "<b>[fix]</b> Fixtures: make it compatible with <b>ylOppTactsPreview (MODIFIED)</b> script.", ], "3.2": [ "<b>[fix]</b> bump version to overwrite the latest version in greasyfork", ], "3.1": [ "<b>[fix]</b> required wrong version of scripts.", ], "3.0": [ "<b>[fix]</b> Fixtures: truncate full names if they are very long.", "<b>[new]</b> make displaying full names in fixture page optional. It is disabled by default. To enable it check 'Display team's full name in fixture' in MZY Settings.", "<b>[Refactor]</b> restructured the project to multiple files.", ], "2.54": [ "<b>[new]</b> Fixtures: add full name of the teams." ], "2.53": [ "<b>[new]</b> show transfer fee in tactic page.", "<b>[fix]</b> Squad Summary: top players table in mobile view.", ], "2.52": [ "<b>[new]</b> show residency days and transfer fee in shortlist." ], "2.51": [ "<b>[new]</b> show transfer fee in more places." ], "2.50": [ "<b>[new]</b> Players Profile: add transfer fee." ], "2.49": [ "<b>[fix]</b> Transfer Market: wrong camp status in test domain" ], "2.48": [ "<b>[fix]</b> download and update urls for userscript were missing." ], "2.47": [ "<b>[fix]</b> some features were not compatible with test version of the site (test.managerzone.com)." ], "2.46": [ "<b>[new]</b> if 'deadline alert' is enabled from MZY Settings, it adds a section to Transfer Monitor to display players you added to monitor their deadline.</b>.", "<b>[fix]</b> clean install was broken." ], "2.45": [ "<b>[new]</b> Market: add optional feature to show training camp status of players. It is disabled by default. To enable it, please select 'Check if player is sent to camp' option from <b>MZY Settings</b>." ], "2.44": [ "<b>[fix]</b> Tables: fix not adding top players to friendly league tables." ], "2.43": [ "<b>[new]</b> Manager Ranking: add value and average top players for each team. You can sort them by this two columns too." ], "2.42": [ "<b>[improve]</b> Squad Summary: add share and market icon for your own players too." ], "2.41": [ "<b>[new]</b> Notebook: add a note icon to MZY Toolbar to open/hide a notebook. It stores your note and you can stick it to a corner to be always be available and visible." ], "2.40": [ "<b>[fix]</b> Transfer: change Fee font color to blue." ], "2.39": [ "<b>[new]</b> Transfer: for non one-club players, the price that current club paid for the player is added next to 'Days at this club'." ], "2.38": [ "<b>[fix]</b> Transfer Filters: delete icon was missing in 'MZY Transfer Filters' modal." ], "2.37": [ "<b>[new]</b> support Managerzone Test Site (test.managerzone.com). It is not fully tested. Please report any issues you encounter in Test site too." ], "2.36": [ "<b>[new]</b> Deadline Alert: add 'Timeout' option in <b>MZY Settings</b> to set deadline timeout. Its value must be between 1 and 360 minutes.", "<b>[new]</b> Deadline Alert: add 'Sound Notification' option to play a bell sound when deadline of at least one of monitored players is less than timeout.", "<b>[fix]</b> Deadline Alert: trash icon was missing in 'MZY Transfer Deadlines' modal when 'van.mz.playerAdvanced' script is enabled.", "<b>[fix]</b> MZY Settings: 'Mark maxed skills' option was missing." ], "2.35": [ "<b>[new]</b> Days at this club: add to player profiles in training report. It is optional and disabled by default. You can enable it from MZY Settings." ], "2.34": [ "<b>[new]</b> <b>(Experimental)</b> Transfer: add deadline alert." ], "2.33": [ "<b>[fix]</b> Federation Front Page: add top players when current federation is changed." ], "2.32": [ "<b>[improve]</b> Federation: first team member sort" ], "2.31": [ "<b>[new]</b> Clash: add average age of top players and teams senior league for each team. this feature is not supported in mobile view." ], "2.30": [ "<b>[fix]</b> Transfer Filters: reset selected H & L checkboxes when Transfer filter is not enabled." ], "2.29": [ "<b>[fix]</b> Hide Players: fixed an issue about hide icon when transfer scout filters are used." ], "2.28": [ "<b>[fix]</b> Days at this club: after v2.27, it was broken in players page." ], "2.27": [ "<b>[new]</b> Transfer Market: it adds a trash icon next to player ID in search result. click on the icon to <b>hide the player. To remove players from hide list, use 'MZY Hide' button." ], "2.26": [ "<b>[improve]</b> Days at this club: it is optional. It is disabled by default. You can enable it from MZY Settings.", "<b>[improve]</b> Player Profile: it stores player profiles in local database to reduce number of requests.", "<b>[improve]</b> Local Database: it deletes outdated local data to reduce the size of database.", "<b>[improve]</b> Transfer: it uses less ajax requests now.", ], "2.25": [ "<b>[new]</b> Training Report: click on player's camp package icon to open its camp report." ], "2.24": [ "<b>[fix]</b> Player Profile: fix Days at this club for injured or suspended players." ], "2.23": [ "<b>[new]</b> Squad Profile: add 'days at this club' to each player profile.", "<b>[fix]</b> Player Comment: show comment icon for players when selected tab changes.", "<b>[fix]</b> Player Comment: change color of comment icon to lightskyblue when player has no comment. (previous color was the same as loyal players background)", ], "2.22": [ "<b>[new]</b> Hire Coaches: adds salary range of each coach. Thanks to <a href=\"https://www.managerzone.com/?p=profile&uid=8577497\">@douglaskampl</a> for suggesting the idea and sharing his implementation." ], "2.21": [ "<b>[new]</b> Club Page: adds total trophy count." ], "2.20": [ "<b>[new]</b> Player Profile: add 'Days at this club' counter." ], "2.19": [ "<b>[new]</b> Squad Summary: it marks players whose skills are shared. click on share icon to see the player in place.", "<b>[new]</b> Squad Summary: it marks players that are in transfer market. click on transfer icon to see the player in market." ], "2.18": [ "<b>[new]</b> show changelog after script update.", "<b>[improve]</b> change icon style of player's comment." ], "2.17": [ "<b>[fix]</b> fixed total skill balls" ], } const MAZYAR_TRANSFER_INTERVALS = { always: { value: "0", label: "always", }, onceMinute: { value: "1", label: "once a minute", }, onceHour: { value: "2", label: "once an hour", }, onceDay: { value: "3", label: "once a day", }, never: { value: "4", label: "never", }, }; const MAZYAR_FILTER_BACKUP_TITLE = "Mazyar Filters Backup\n\n"; const MAZYAR_DEADLINE_ALERT_SOUND = "data:audio/mpeg;base64,//OExAAAAAAAAAAAAFhpbmcAAAAPAAAAKAAADbAAAQEfHy4uLjMzOTk5Pz9CQkJISE9PT2ZmbW1tc" + "3N2dnZ6eoCAgImJlJSUo6OoqKiurrOzs7m5v7+/xMTKysrNzdHR0dXV2dnZ3Nzg4ODk5Ofn5+vr7+/v8/P09PT4+P7+/v//AAAAC" + "kxBTUUzLjEwMARIAAAAAAAAAAAVCCQCzCEAAZoAAA2wXtypYQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//MUxAAAAAP8AUAAAACAS" + "QCQRgCASLq7//PExAsjqwbDH4dpIB1CMEAD6dTAceIh3ZSwkBKkv/gDUEnGWPf6boppjjHOHIAuAsH0EGMjxmOcpE8OeRA5gmYKe" + "CRoflaB40QTHmHMOl8RgYoXMFb3/SOjwUaLL7HAtYywQgSgviYEoaEv/+ipSKZcVTMDRMe5THoShocHAPApjgHgX///Wp//5fNwu" + "g2EuseMh+mJdpVnl7fQKJA0AAMYlm5M3efLtmZ5pmpiPi+lPeGNDVo8maLthyIjQOnQUKcHBPHPC86JxSY1y6Sw0DATakZLSWQIZ" + "YrmIrUZwslwgRZJ4umKZVMiKmpDSdZIgIBAhGatZYC1Qad7A3THKNEBeA2MjtLpeOEy8bhJn18nSyH7D0ntaktEyNiZI4eiqY/0r" + "M9ev67OOoMGn//2MgAZP//KICzT1ZkAcAABn2BbgANQeBkFvylb//5MjaS/orSRpfqp7X5xHv//0SZJEBsJ1NH+y1f//rMP/Q9PS" + "/WuqqizrYvGxSFbDjHaBBgs//OExO8p2wbjH5iYJOJ4qtwAAAd9gqwA9A7N6hlx3f+r/3X3TQsZD0l/+v9Y6gWlEMNm/6///zpn/" + "6f////uxsMqBiwYUAn2Az3CwB8FkFDf/6cw4y2nY4bOvdP/6hugHgOmr/3dL//+r//////1qGuBiuQGmggAAcDXgDv+Bjf///+kg" + "MUn//+cCbu//t//+pf//////RckRrGsX2oAAe4DfcOQAHqcv9fkAZ/czPTqellTejNNGFToMr//UZgJ//M0xPoSaqa7H8WYAWMG/" + "+v//6lf/////+pZEAHgTqoCCS0j+oDQP7J9Z/WgggdSUibTXdaisMiHVLL5RFjRIJ5fJFCmuQia//M0xOsOAqqqXhAoNIjQjPrPZ" + "tFdxg1xmTp2p1L9loLFCAD1Fbn2/TVe3//rX//////9h1hQxQa/b/YDeAGgAbtBQEACJhTQ//M0xO4LYqKkXAKm9bUQ42ig/jRkF" + "GZrOMIGC5Uy5EcPBywwRQCBzKjyEcTDDAixkUZIQOi5NK44/C80mmu3+all2fQkIUl+//MkxPsJ4qa0ngCa1JahVCjRgAozJgDMA" + "DFqTAkDYJjBnxYEAm4GADplB4wwhBKn//M0xPYNEqKmXgHanFFkGsGPGjp43yQxM06JcvIrQ+ymjPH5fNiEEwM7kzJIfjzAGoI3p" + "Jx8CiROuscgABwRW9uTOUAYgIky//NExPwUwqaaXGsnKAZAVClUdJKAsGRoAo1NgctjDQLeMvpImtYyhIBIWl+y1A4UFfQwYSOBB" + "2wQhH1NY0OHIgtYEIlcnRD5aili8Qh1QNoLjuAo6DhsYZKimsAqNyKF//OkxPxF4r6SXtY1yOh5Ibl8ELEVQSAMJyWB0vRoAJmRa" + "djNIAg6X9rrqYbUqwACcACOKgICPYPFjrprgWaVimlY1hv/Q9/rak6Ojdf/Wyklo//9JLRBSDdH7KU6P0bfo+pKjrbUl/baLmzJc" + "dCUUxFiSk1Ke1W0ucxEp0lH4CBHgS2HCIAfDygAfu3m0N//+NDP/9BFT///rH4DXzA+3+rf//1f//////TLgucLoi3oKuAAAMH4t" + "wAcPx1Dt////d/WpPf//6A6wgwi3/V///U3/r////6KKRdJcJgEDNUAB4H41wA+MHfXtVs3/s6e79///TLgBEldf/Z///1t///+v" + "9/61GZIBjhxagAB/gAOB5QAdxon18ix/////fqT//NExPcWaqLW/kNZHXV///Y3AVROK+rfu3//W///////sfJYMjDEertpfgd8w" + "JQAm4tHEMMSAc3UyVhQDjgIr6DEEHtN76ziJmkpTE2keNzhVQLpsYLJsyQTL5F5MIUN//M0xPALiqa2niglJP7/0CYIIFYFMpk+n" + "/X//qdWgimhQ3Qp7skymNzA0mCBoX01WW3QroHSkSwFHjIn15mIeoCQeFVAB/4y//MkxPwLWqauPggmGLeAVAP5OT0hhleGHOcFi" + "RHPH/hue6ablgXIlIcVqxnx7UeG//MkxPEK2qasngCk8OjfX/4zZwQDHQS1a02UyClKYcwgh4gAyhU6ygHJAsb2Bvcc//M0xOgLo" + "qaqXgBiiMGYIoeJguglEKFN1K/GZSLyOvDAAnAQUEoFQToRQ0Of///oMpBkNi+bm+sjD//84Mgaf/dhYAbnil3e//NUxPQZ+qqeX" + "0OYAIu5vYnYhYa3/hgUkTQAAbuDis9Wrqkaa0nWrVZfTbyuBdVBAYa4thFy0MsiUiSFmg3QYjY+i1I0L1B6pRLBsYECGuXSDEWJ8" + "mVlhFjInjcZkbw7gxidcxJJAmtaisRtFFSY5A7kbpKDEY+k//NkxPciswbDHZiYALnQacNMNyVK5FzE1dFSJiYjicnnyRMhBUiJ4" + "umubGJkmXVHUCmVRzjL/qXW31LQe51ILsHK//qFv//zMFgbqpiAgAAAi2g8gAGDweFyK1e1f//izCfb6m7f9TPq3WWTZ1Kb6/+iT" + "IfqARgypBTX/t//9ZgNd2/rUr////1E0MsDdxEc//OExO8qiwbjH5iIBLXgADug8APN6h3CZv///0evsmeZbt//1JkwBR5JJ/q1f" + "//zhKf/////+s4NcEEyfUoADa8GwAfCYHif/r9+6PW93Z0BsoHGa739f1oEPADIiCv+p///zIa7f9v////RSNSBAIgQM1UADUYDb" + "8OQAEwxD8wNP1CnjSf9Jbf9TKSOo2QSSL5QJl1s3qfo/uaA8hPo/60av//pkx//////7JmAmZk6/G//E3AF6vW/xXAiV21P//M0x" + "PcRGqa3H8WQATzz36up5+nn0lSILaN16NRv5kAD88l/6///yse///6000EKqy+9mXb2UakuJCoAD4ULfcH8AGIQif4h//M0xO0L2" + "qap/BAmGAvjGjs/WtkUdlooUlIsnZ9svkk6ls7qd2R/6hugVhw8//b/+ikkk5iPD//r3U/UlMS6zJLWpJKroF8X//M0xPgNSqagf" + "gKk4CBsw8PVHAtgA+DeGjf///+hglki///0x0Adjk2/1UF///5gO7//////qWZD5CRy6pUQwH/AHzxCf/////M0xP0PKqaiXgBmb" + "P6mLX///SBHjD/mMp1//+UFn/////+k04lAOAoDqgwHA9wA+JQd////+gUC6h///ogjgm2+1f///UZf//M0xPsPEp6sfgKavP///" + "+3+mOgWUaUCBwBoAPkoPP////qW5SHf///0gFWXH////+o9//////+gvB1VABkBgPsAPjgAKd3///M0xPkTCqaqXgBibP//1NF3/" + "//KwLhNv1N///1mX//////2L5MdEOBLSB8Ihf////+oHjZ///6zAIG/9////O///////j5K//M0xOcK0qakHgKm8MeVIOAMyB8aD" + "P////6KEn///WAzlf////qMv//////zo7Ug4HoQHyYz////+g8GW///zAD1O/7f//9S//MkxPYJ2qK0PgRU+P//////+VOqCQcCw" + "ID5dHr///9/Uw1C7///0gGSMP9v///J//MkxPEJqqKsXgHk5P//////x2oAQDgCBAfEIFv///1N8qobf///QA4mf////9f///Mkx" + "O0JIqKwXgRU+P/////MagFwOBvIB8zGA/////Wohf//84FOh/p///1J//////MkxOsJcqK4ngHa8P//oCoDBwLQwPhQsf////xXO" + "///6gEF93/p///6ajDAD5MG//MkxOgIkqKsPgHg8H////6GCz///nASdX/////9dRwH8uDah////7IJic///8vg//MkxOgH8p6wP" + "gHa8MBn7/////9SAD0AoVepE/8XwqrChgzJftMv/NjGih/1PRZI//MkxOsICqK0PgHa8MieM/YCokdCCEqCkwFm4UeF46akNIKKG" + "HJb/b/IqYF0EhE3//MkxO0IYqKoXgSE+P//pkxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//MkxO4IUp6sfgHa8Kqqq" + "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//MkxO8IIp64fgNE+Kqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" + "qqqqqqq//MkxPEGUW60XgCU4Kqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//MUxPoFmW6oOAHa8Kqqqqqqqqqqq" + "qqq//MkxO4GKW6gGUBoAKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//M0xPgQMcqoeYCgAKqqqqqqqqqqqqqqq" + "qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//MUxPIAAAP8AYAAAKqqqqqqqqqqqqqq"; /* *********************** Squad - Icons (Shared Skills & Transfer) ********************************** */ function squadAddIconsHeaderToSummaryTable(table, ownView = false) { if (ownView) { const age = table.querySelector("thead tr th:nth-child(5) a"); age.innerText = age.innerText[0]; const born = table.querySelector("thead tr th:nth-child(6) a"); born.innerText = born.innerText[0]; } const th = document.createElement("th"); th.style.width = "0px"; const target = table.querySelector("thead tr th:nth-child(2)"); target.parentNode.insertBefore(th, target); } function squadAddIconsBodyToSummaryTable(player, info) { const td = document.createElement("td"); if (info.shared) { const icon = mazyarCreateSharedIcon("Click to see player profile."); icon.style.fontSize = "13px"; icon.classList.add("special_player"); icon.addEventListener("click", () => { mazyar.showPlayerInModal(info.detail); }) td.appendChild(icon); } if (info.market) { const icon = mazyarCreateMarketIcon("Player is in Transfer Market."); icon.style.fontSize = "13px"; icon.classList.add("special_player"); const link = document.createElement("a"); link.href = info.marketLink; link.target = "_blank"; link.appendChild(icon); td.appendChild(link); } const target = player.querySelector("td:nth-child(2)"); target.parentNode.insertBefore(td, target); } async function squadInjectIconsToSummaryTable(table) { const players = table?.querySelectorAll("tbody tr"); const ownView = players?.[0]?.children?.length > 6; const text = document.createElement("div"); text.innerHTML = `<b>MZY:</b> fetching players' info ...`; table.parentNode.insertBefore(text, table); const teamId = mazyarExtractTeamId(document.baseURI); const playersInfo = await mazyarExtractPlayersProfileDetails(teamId); if (!playersInfo) { text.innerHTML = `<b>MZY:</b> fetching players' info ...<span style="color: red;"> failed</span>.`; return; } text.innerHTML = `<b>MZY:</b> fetching players' info ...<span style="color: green;"> done</span>.`; squadAddIconsHeaderToSummaryTable(table, ownView); for (const player of players) { const name = player.querySelector("a"); const playerId = mazyarExtractPlayerIdFromProfileLink(name?.href); squadAddIconsBodyToSummaryTable(player, playersInfo[playerId]); } } /* *********************** Squad - Residency ********************************** */ function squadAddDaysAtThisClubToPlayerProfile() { if (mazyar.isDaysAtThisClubEnabledForPlayerProfiles()) { const { days, price } = mazyarExtractResidencyDaysAndPrice(document); const daysDiv = document.createElement("div"); if (days >= 0) { const text = days === 0 ? 'N/A' : `≤ ${days}`; daysDiv.innerHTML = `Days at this club: <strong>${text}</strong>`; daysDiv.classList.add("mazyar-days-at-this-club"); if (price) { daysDiv.innerHTML += ` <span style="margin-left: 25px;">Transfer Fee: <strong style="color: blue;">${price}</strong><span>`; } } else if (mazyar.isDaysAtThisClubEnabledForOneClubPlayers()) { const text = 'Entire Career'; daysDiv.innerHTML = `Days at this club: <strong>${text}</strong>`; daysDiv.classList.add("mazyar-days-at-this-club", "mazyar-days-entire"); } const profile = document.querySelector("div.playerContainer"); profile?.appendChild(daysDiv); } } /* *********************** Squad - Total Balls ********************************** */ function squadSortPlayersByTotalSkillBalls(th) { const table = document.getElementById("playerAltViewTable"); const players = table?.querySelectorAll("tbody tr"); if (table.ascending) { table.ascending = false; const icon = th.querySelector("i"); icon.classList.value = ''; icon.classList.add("fa-solid", "fa-sort-down"); icon.style.color = "#000"; const rows = [...players].sort((a, b) => a.skillBalls - b.skillBalls); table.querySelector("tbody").replaceChildren(...rows); } else { table.ascending = true; const icon = th.querySelector("i"); icon.classList.value = ''; icon.classList.add("fa-solid", "fa-sort-up"); icon.style.color = "#000"; const rows = [...players].sort((a, b) => b.skillBalls - a.skillBalls); table.querySelector("tbody").replaceChildren(...rows); } } function squadAddTotalSkillBallsHeaderToSummaryTable(table) { const th = document.createElement("th"); th.title = "Total Skill Balls"; th.innerHTML = `<i aria-hidden="true" style="display: inline; font-size: 11px; color: #555;" class="fa-solid fa-sort"></i><span style="font-size: 11px;">T</span>`; th.style.textDecoration = "none"; th.style.textAlign = "right"; const target = table.querySelector("thead tr th:last-child"); target.parentNode.insertBefore(th, target); th.addEventListener("click", () => { squadSortPlayersByTotalSkillBalls(th); }); } function squadAddPlayerTotalSkillBallsToSummaryTable(player, sport) { let sum = 0; const start = sport === "soccer" ? 6 : 7; const end = 17; for (const skill of [...player.children].slice(start, end)) { sum += Number(skill.innerText); } const td = document.createElement("td"); td.innerText = sum.toString(); td.style.fontWeight = `${Math.ceil(900 * sum / 110)}` td.style.textAlign = "right"; const target = player.querySelector("td:last-child"); target.parentNode.insertBefore(td, target); player.skillBalls = sum; } function squadAddTotalSkillBallsToSummaryTable(table) { const sport = mazyarExtractSportType(); const players = table?.querySelectorAll("tbody tr"); if (players?.[0]?.children?.length > 6) { squadAddTotalSkillBallsHeaderToSummaryTable(table); for (const player of players) { squadAddPlayerTotalSkillBallsToSummaryTable(player, sport); } } } /* *********************** Squad - Top Players ********************************** */ function squadCreateCompactElement(title, value) { const dd = document.createElement("dd"); dd.innerHTML = `<span class="listHeadColor">${title}</span><span class="clippable">${value}</span>`; return dd; } function squadSummaryCreateCompactRow(row, currency = "USD", sport = "soccer") { const dl = document.createElement("dl"); dl.classList.add("hitlist-compact-list", "columns"); dl.appendChild(squadCreateCompactElement("Count", row.count)); dl.appendChild(squadCreateCompactElement("Total", `${mazyarFormatBigNumber(row.all)} ${currency}`)); if (sport == "soccer") { dl.appendChild(squadCreateCompactElement("Top 16", `${mazyarFormatBigNumber(row.top16)} ${currency}`)); dl.appendChild(squadCreateCompactElement("Top 11", `${mazyarFormatBigNumber(row.top11)} ${currency}`)); } else { dl.appendChild(squadCreateCompactElement("Top 21", `${mazyarFormatBigNumber(row.top21)} ${currency}`)); } return dl; } function squadCreateTopPlayersTable(rows, currency = "USD", sport = "soccer") { const thead = document.createElement("thead"); const tr = document.createElement("tr"); const titleHeader = document.createElement("th"); titleHeader.classList.add("header"); titleHeader.innerText = "Group"; titleHeader.style.textAlign = "center"; titleHeader.style.textDecoration = "none"; const countHeader = document.createElement("th"); countHeader.classList.add("header"); countHeader.innerText = "Count"; countHeader.title = "Number of Players"; countHeader.style.textAlign = "center"; countHeader.style.textDecoration = "none"; const totalHeader = document.createElement("th"); totalHeader.classList.add("header"); totalHeader.innerHTML = "Total"; totalHeader.title = "Total Value of Players"; totalHeader.style.textAlign = "center"; totalHeader.style.textDecoration = "none"; tr.appendChild(titleHeader); tr.appendChild(countHeader); tr.appendChild(totalHeader); if (sport === "soccer") { const top16Header = document.createElement("th"); top16Header.classList.add("header"); top16Header.innerHTML = "Top 16"; top16Header.title = "Value of Top 16 Players"; top16Header.style.textAlign = "center"; top16Header.style.textDecoration = "none"; const top11Header = document.createElement("th"); top11Header.classList.add("header"); top11Header.innerHTML = "Top 11"; top11Header.title = "Value of Top 11 Players"; top11Header.style.textAlign = "center"; top11Header.style.textDecoration = "none"; tr.appendChild(top16Header); tr.appendChild(top11Header); } else { const top21Header = document.createElement("th"); top21Header.classList.add("header"); top21Header.innerHTML = "Top 21"; top21Header.title = "Value of Top 21 Players"; top21Header.style.textAlign = "center"; top21Header.style.textDecoration = "none"; tr.appendChild(top21Header); } thead.appendChild(tr); const tbody = document.createElement("tbody"); for (const row of rows) { const title = document.createElement("td"); title.innerHTML = `${row.title}`; title.classList.add("hitlist-compact-list-column"); const compact = squadSummaryCreateCompactRow(row, currency, sport); title.appendChild(compact); const count = document.createElement("td"); count.innerHTML = `${row.count}`; count.style.textAlign = "center"; const all = document.createElement("td"); all.innerText = `${mazyarFormatBigNumber(row.all)} ${currency}`; all.style.textAlign = "end"; const tr = document.createElement("tr"); tr.appendChild(title); tr.appendChild(count); tr.appendChild(all); tbody.appendChild(tr); if (sport === "soccer") { const top16 = document.createElement("td"); top16.innerText = `${mazyarFormatBigNumber(row.top16)} ${currency}`; top16.style.textAlign = "end"; const top11 = document.createElement("td"); top11.innerText = `${mazyarFormatBigNumber(row.top11)} ${currency}`; top11.style.textAlign = "end"; tr.appendChild(top16); tr.appendChild(top11); } else { const top21 = document.createElement("td"); top21.innerText = `${mazyarFormatBigNumber(row.top21)} ${currency}`; top21.style.textAlign = "end"; tr.appendChild(top21); } } const table = document.createElement("table"); table.classList.add("mazyar-table", "tablesorter", "hitlist", "marker", "hitlist-compact-list-included"); table.appendChild(thead); table.appendChild(tbody); return table; } function squadInjectTopPlayersTable(table) { const sport = mazyarExtractSportType(document); const currency = mazyarExtractClubCurrency(document); const players = mazyarExtractClubPlayersDetails(document, currency); const summary = mazyarExtractSquadSummaryDetails(players, sport); const topPlayers = squadCreateTopPlayersTable(summary, currency, sport); topPlayers.style.marginBottom = "10px"; const div = document.createElement("div"); div.classList.add("mazyar-flexbox-column"); div.appendChild(topPlayers); div.style.marginTop = "10px"; const target = table.parentNode.parentNode; target.parentNode.insertBefore(div, target); } function squadInjectInformation(table) { squadAddTotalSkillBallsToSummaryTable(table); squadInjectIconsToSummaryTable(table); squadInjectTopPlayersTable(table); } function squadInjectInformationToSummary() { const target = document.getElementById('squad_summary'); if (target) { if (target.style.display !== 'none') { // visiting squad summary directly const table = document.querySelector("table#playerAltViewTable"); if (table && !table.injecting) { table.injecting = true; squadInjectInformation(table); } } // add observer if user changes from other tabs to squad summary tab const callback = () => { const table = document.querySelector("table#playerAltViewTable"); if (table && !table.injecting) { table.injecting = true; squadInjectInformation(table); } }; const observer = new MutationObserver(callback); const config = { childList: true, subtree: true }; observer.observe(target, config); } } function calendarInject() { if (document.getElementById('calendar-new')) { // add observer if user changes from other tabs to squad summary tab const callback = (mutationsList,) => { for (let mutation of mutationsList) { if (mutation.type === 'childList') { const modal = document.getElementById('lightbox_calendar-administration'); if (modal && !modal.injecting) { modal.injecting = true; mazyar.injectToCalendar(modal); } } } }; const observer = new MutationObserver(callback); const config = { childList: true }; observer.observe(document.body, config); } } function squadInjectInformationToProfiles() { if (document.baseURI.search("/?players&pid=") > -1) { squadAddDaysAtThisClubToPlayerProfile(); mazyar.addPlayerComment(); } else { const target = document.getElementById('squad_profiles'); if (target) { if (target.style.display !== 'none') { // visiting profiles directly const container = document.getElementById('players_container'); if (container && !container.injecting) { container.injecting = true; mazyar.addDaysAtThisClubToAllPlayers(container); mazyar.addPlayerComment(); } } // add observer if user changes from other tabs to squad profiles tab const callback = () => { const container = document.getElementById('players_container'); if (container && !container.injecting) { container.injecting = true; mazyar.addDaysAtThisClubToAllPlayers(container); mazyar.addPlayerComment(); } }; const observer = new MutationObserver(callback); const config = { childList: true, subtree: true }; observer.observe(target, config); } } } /* *********************** Rankings ********************************** */ function createSortableHeaderCell(cellText = "", options = { title: "", textAlign: "", marginBetween: "" }) { const cell = document.createElement("th"); const sortIcon = document.createElement("i"); const text = document.createElement("span"); cell.appendChild(sortIcon); cell.appendChild(text); cell.style.textDecoration = "none"; sortIcon.style.display = "inline"; sortIcon.style.fontSize = "11px"; sortIcon.style.color = "#555"; sortIcon.classList.add("fa-solid", "fa-sort", "mazyar-sort-icon"); sortIcon.setAttribute("aria-hidden", "true"); text.style.fontSize = "11px"; text.innerText = cellText; if (options?.marginBetween) { text.style.marginLeft = options.marginBetween; } if (options?.title) { cell.title = options.title; } if (options?.textAlign) { cell.style.textAlign = options.textAlign; } return cell; } function resetMazyarSortIcons(table) { const sortIcons = table.querySelectorAll("thead th i.mazyar-sort-icon"); for (const icon of sortIcons) { icon.classList.value = ''; icon.classList.add("fa-solid", "fa-sort", "mazyar-sort-icon"); } } function rankingSortTeamsByKey(table, th, key) { resetMazyarSortIcons(table); const teams = table?.querySelectorAll("tbody tr"); if (table.ascending) { table.ascending = false; const icon = th.querySelector("i"); icon.classList.value = ''; icon.classList.add("fa-solid", "fa-sort-down", "mazyar-sort-icon"); icon.style.color = "#000"; const rows = [...teams].sort((a, b) => a[key] - b[key]); table.querySelector("tbody").replaceChildren(...rows); } else { table.ascending = true; const icon = th.querySelector("i"); icon.classList.value = ''; icon.classList.add("fa-solid", "fa-sort-up", "mazyar-sort-icon"); icon.style.color = "#000"; const rows = [...teams].sort((a, b) => b[key] - a[key]); table.querySelector("tbody").replaceChildren(...rows); } } function rankingAddSquadHeaders(table) { if (table.querySelector(".mazyar-ranking-value-header")) return; const header = table.querySelector("thead tr"); const value = createSortableHeaderCell("Value", { textAlign: "center", marginBetween: "3px" }); const age = createSortableHeaderCell("Age", { textAlign: "center", marginBetween: "2px" }); value.classList.add("mazyar-ranking-value-header"); age.classList.add("mazyar-ranking-age-header"); header.append(value, age); value.addEventListener("click", () => rankingSortTeamsByKey(table, value, "topPlayersValue")); age.addEventListener("click", () => rankingSortTeamsByKey(table, age, "topPlayersAge")); } async function rankingLoadTeamInfo(team, value, age) { value.replaceChildren(mazyarCreateLoadingIcon2()); age.replaceChildren(mazyarCreateLoadingIcon2()); const link = team.querySelector('a[href*="p=team"][href*="tid="]'); const teamId = mazyarExtractTeamId(link?.href); try { if (!teamId) throw new Error("Missing team id"); const url = mazyarGetSquadSummaryUrl(teamId) + "&mzc=" + Date.now(); const doc = await mazyarFetchHtml(url); const responseLink = doc?.querySelector('a[href*="p=players"][href*="tid="]'); const responseTeamId = mazyarExtractTeamId(responseLink?.href); if (!doc || String(responseTeamId) !== String(teamId)) throw new Error("Team response mismatch"); const currency = mazyarExtractClubCurrency(doc); ({ values: team.topPlayersValue, avgAge: team.topPlayersAge } = mazyarExtractClubTopPlyers(doc)); value.innerHTML = `<strong>${mazyarFormatBigNumber(team.topPlayersValue, ",")}</strong> ${currency}`; age.innerHTML = `<strong>${mazyarFormatAverageAge(team.topPlayersAge)}</strong>`; } catch (error) { console.warn("[MZY ranking]", teamId, error); value.innerHTML = '<button type="button" class="mzc-ranking-retry">Retry</button>'; age.textContent = "—"; value.querySelector(".mzc-ranking-retry")?.addEventListener("click", () => rankingLoadTeamInfo(team, value, age)); } } async function rankingInjectTeamInfo(team) { if (team.querySelector(".mazyar-ranking-value")) return; const value = document.createElement("td"); const age = document.createElement("td"); value.className = "mazyar-ranking-value"; age.className = "mazyar-ranking-age"; value.style.cssText = age.style.cssText = "white-space:nowrap;text-align:center"; team.append(value, age); team.topPlayersValue = 0; team.topPlayersAge = 0; await rankingLoadTeamInfo(team, value, age); } async function rankingInjectSquadValue() { const table = document.getElementById("userRankTable"); if (!table || table.dataset.mzcRankingLoading === "true") return; const teams = [...table.querySelectorAll("tbody tr")]; if (!teams.length) return; table.dataset.mzcRankingLoading = "true"; rankingAddSquadHeaders(table); const queue = teams.filter((team) => !team.querySelector(".mazyar-ranking-value")); const workers = Array.from({ length: Math.min(4, queue.length) }, async () => { while (queue.length) await rankingInjectTeamInfo(queue.shift()); }); await Promise.all(workers); table.dataset.mzcRankingLoading = "false"; } /* *********************** Clash ********************************** */ async function clashFetchAndTeamLeagueAndFlag(team) { const url = `https://${location.hostname}/?p=team&tid=${team.teamId}`; const doc = await mazyarFetchHtml(url); if (doc) { const leagueRow = doc.querySelector("#infoAboutTeam > dd:nth-child(6)"); const flag = leagueRow.querySelector("img"); const seriesName = leagueRow.querySelector("span:last-child"); team.querySelector("td.flag").appendChild(flag); team.querySelector("td.league").appendChild(seriesName); } } async function clashFetchAndUpdateTeamsInfo(team, mobileView) { if (!mobileView) { clashFetchAndTeamLeagueAndFlag(team); } const info = { currency: "", averageAge: 0, }; const doc = await mazyarFetchHtml(team.url); let successful = false; if (doc) { info.currency = mazyarExtractClubCurrency(doc); ({ values: team.topPlayersValue, avgAge: info.averageAge } = mazyarExtractClubTopPlyers(doc)); successful = true; } else { team.topPlayersAverageAge = 0; } team.querySelector("td.value").innerHTML = successful ? `${mazyarFormatBigNumber(team.topPlayersValue, ",")} ${info.currency}` : 'N/A'; if (!mobileView) { team.querySelector("td.age").innerHTML = successful ? `<strong>${mazyarFormatAverageAge(info.averageAge)}</strong>` : 'N/A'; } return successful; } function clashSortTeams(teams) { teams.sort((a, b) => b.topPlayersValue - a.topPlayersValue); let rank = 0; for (const team of teams) { rank++; team.className = rank % 2 ? "odd" : "even"; const target = team.querySelector("button.mazyar-donut.rank"); if (target) { target.classList.remove("mazyar-loading-donut"); target.classList.add("mazyar-final-donut"); target.innerText = `${rank}`; } } const newOrderWithPlayedMatches = []; for (const row of teams) { newOrderWithPlayedMatches.push(row); for (const playedMatch of row.playedMatches) { playedMatch.className = row.className; newOrderWithPlayedMatches.push(playedMatch); } } const tbody = document.querySelector("div.panel-2 table tbody"); tbody.replaceChildren(...newOrderWithPlayedMatches); } async function clashCalculateRankOfTeams(teams, mobileView) { const jobs = []; for (const team of teams) { jobs.push(clashFetchAndUpdateTeamsInfo(team, mobileView)); } const results = await Promise.all(jobs); if (results.every(Boolean)) { // data for all teams are fetched successfully clashSortTeams(teams); } else { // data for some teams are not fetched for (const team of teams) { const target = team.querySelector("button.mazyar-donut.rank"); target.classList.remove("mazyar-loading-donut"); target.classList.add("mazyar-final-donut"); target.innerText = `-`; } } } function clashAddRankElements(team, mobileView) { if (!mobileView) { const league = document.createElement("td"); league.style.whiteSpace = "collapse"; league.style.width = "max-content"; league.innerText = ""; league.classList.add("league"); league.style.textAlign = "center"; team.insertBefore(league, team.firstChild); const flag = document.createElement("td"); flag.style.width = "max-content"; flag.innerText = ""; flag.classList.add("flag"); flag.style.textAlign = "center"; team.insertBefore(flag, team.firstChild); const age = document.createElement("td"); age.style.width = "max-content"; age.innerText = ""; age.classList.add("age"); age.style.textAlign = "center"; team.insertBefore(age, team.firstChild); } const value = document.createElement("td"); value.style.width = "max-content"; value.innerText = ""; value.classList.add("value"); value.style.textAlign = "right"; team.insertBefore(value, team.firstChild); const rank = document.createElement("td"); rank.style.width = "max-content"; team.insertBefore(rank, team.firstChild); const button = document.createElement("button"); button.innerText = "_"; button.classList.add("mazyar-donut", "mazyar-loading-donut", "rank", "fix-width"); button.title = "Click to see squad summary"; rank.appendChild(button); button.onclick = () => { mazyar.displaySquadSummary(team.url); }; } function clashInjectRanks() { const table = document.querySelector("table.hitlist.challenges-list"); const headers = table.querySelector("thead tr"); const mobileView = !headers; // mobile view has not headers section if (headers) { const league = document.createElement("th"); league.style.textAlign = "center"; league.innerText = "League"; league.style.whiteSpace = "collapse"; league.style.width = "11%"; headers.insertBefore(league, headers.firstChild); const flag = document.createElement("th"); flag.style.textAlign = "center"; flag.innerText = ""; flag.style.width = "2%"; headers.insertBefore(flag, headers.firstChild); const age = document.createElement("th"); age.style.textAlign = "center"; age.innerText = "Age"; age.title = "Average Age of Top Players"; age.style.width = "4%"; headers.insertBefore(age, headers.firstChild); const values = document.createElement("th"); values.style.textAlign = "center"; values.innerText = "Values"; values.title = "Top Players Total Values"; values.style.width = "13%"; headers.insertBefore(values, headers.firstChild); const rank = document.createElement("th"); rank.style.textAlign = "center"; rank.innerText = "Rank"; rank.title = "Team Rank in This Federation"; rank.style.width = "3%"; headers.insertBefore(rank, headers.firstChild); } const rows = table.querySelectorAll("tbody tr"); for (const row of rows) { // in mobile view played challenges are also <tr> and for this rows, the team name is not a hyperlink const name = row.querySelector("a.team-name"); if (name?.href) { // this is info row row.teamId = mazyarExtractTeamId(name.href); row.url = mazyarGetSquadSummaryUrl(row.teamId); clashAddRankElements(row, mobileView); row.playedMatches = []; } else { // this is match row (in mobile view) // expand to match the previous row row.querySelector("td").colSpan = mobileView ? "3" : "6"; row.previousSibling.playedMatches?.push(row); } } const teams = [...rows].filter((team) => team.url?.length > 0); clashCalculateRankOfTeams(teams, mobileView); } /* *********************** Federation Page ********************************** */ function federationGetUsername(node) { return node?.querySelector("a")?.innerText; } async function federationUpdateMemberInfo(member, username, sport) { let values = 0; let currency = ""; const url = `https://${location.hostname}/xml/manager_data.php?username=${username}`; const doc = await mazyarFetchXml(url); const team = { id: doc?.querySelector(`Team[sport="${sport}"]`).getAttribute("teamId"), name: doc?.querySelector(`Team[sport="${sport}"]`).getAttribute("teamName"), } if (team.id) { const squadUrl = mazyarGetSquadSummaryUrl(team.id); const doc = await mazyarFetchHtml(squadUrl); if (doc) { currency = mazyarExtractClubCurrency(doc); values = mazyarExtractClubTopPlyers(doc).values; } } const name = document.createElement("div"); name.style.color = "blue"; name.style.width = "100%"; name.style.marginTop = "0.5em"; name.title = team.name; name.innerHTML = `<strong style="color:black;">Team: </strong>${team.name.length > 20 ? team.name.substring(0, 16) + " >>>" : team.name}`; member.querySelector("td").appendChild(name); const value = document.createElement("div"); value.style.color = "blue"; value.style.width = "100%"; value.style.marginTop = "0.5em"; value.innerHTML = `<strong style="color:black;">Top${sport === "soccer" ? 11 : 21}: </strong>${mazyarFormatBigNumber(values, ",")} ${currency}`; member.querySelector("td").appendChild(value); const separator = document.createElement("hr"); separator.style.marginBottom = "-3px"; member.querySelector("td").appendChild(separator); return { member, values, currency }; } function federationGetTableHeader(target) { const thead = target.querySelector("thead td"); return thead.innerText; } function federationSetTableHeader(target, text) { const thead = target.querySelector("thead td"); thead.innerText = text; } async function federationSortTeamsByTopPlayers(target) { const sport = mazyarExtractSportType(); const jobs = []; const tbody = target?.querySelector(" tbody"); for (const member of tbody.children) { const username = federationGetUsername(member); if (username) { jobs.push(federationUpdateMemberInfo(member, username, sport)); } } if (jobs.length > 0) { const tableHeader = federationGetTableHeader(target); let dots = 0; const loadingInterval = setInterval(() => { federationSetTableHeader(target, tableHeader + " " + ".".repeat(1 + (dots % 3))); dots++; }, 1000); const members = await Promise.all(jobs); clearInterval(loadingInterval); federationSetTableHeader(target, tableHeader + " ▼"); members.sort((a, b) => b.values - a.values); const newOrder = members.map((t) => t.member); let rank = 0; for (const row of newOrder) { rank++; row.className = rank % 2 ? "odd" : "even"; } tbody.replaceChildren(...newOrder); let totalValue = 0; for (const member of members) { totalValue += member.values; } const total = document.createElement("tr"); total.style.color = "blue"; total.style.textAlign = "center"; total.style.width = "100%"; total.innerHTML = `<td><strong style="color:black;">Total: </strong>${mazyarFormatBigNumber(totalValue, ",")} ${members[0].currency}</td>`; tbody.appendChild(total); } } function federationFrontPage() { const content = document.getElementById('federation-content'); if (content) { const target = document.getElementById("federation_clash_members_list"); if (target && !target.topPlayersInjected) { target.topPlayersInjected = true; federationSortTeamsByTopPlayers(target); } const callback = () => { const target = document.getElementById("federation_clash_members_list"); if (target && !target.topPlayersInjected) { target.topPlayersInjected = true; federationSortTeamsByTopPlayers(target); } }; const observer = new MutationObserver(callback); const config = { childList: true, subtree: true }; observer.observe(content, config); } } /* *********************** Match ********************************** */ // ---------------- Top Players ------------- function matchCreateTopPlayersHeader(sport = "soccer") { const thead = document.createElement("thead"); const tr = document.createElement("tr"); const title = document.createElement("th"); const count = document.createElement("th"); const tops = document.createElement("th"); const age = document.createElement("th"); const playerCount = sport === "soccer" ? 11 : 21; title.classList.add("header"); title.innerText = "Group"; title.style.textAlign = "center"; title.style.textDecoration = "none"; count.classList.add("header"); count.innerText = "Count"; count.title = "Number of Players"; count.style.textAlign = "center"; count.style.textDecoration = "none"; tops.classList.add("header"); tops.innerHTML = `Top ${playerCount}`; tops.title = "Value of Top 11 Players"; tops.style.textAlign = "center"; tops.style.textDecoration = "none"; age.classList.add("header"); age.innerHTML = "Average Age"; age.title = `Average Age of Top ${playerCount} Players`; age.style.textAlign = "center"; age.style.textDecoration = "none"; tr.appendChild(title); tr.appendChild(count); tr.appendChild(tops); tr.appendChild(age); thead.appendChild(tr); return thead; } function matchCreateTopPlayersBody(rows, currency = "USD", sport = "soccer") { const tbody = document.createElement("tbody"); for (const row of rows) { const tr = document.createElement("tr"); const title = document.createElement("td"); const count = document.createElement("td"); const top = document.createElement("td"); const age = document.createElement("td"); title.innerHTML = `${row.title}`; title.classList.add("hitlist-compact-list-column"); count.innerHTML = `${row.count}`; count.style.textAlign = "center"; top.style.textAlign = "end"; age.style.textAlign = "center"; if (sport === "soccer") { top.innerText = `${mazyarFormatBigNumber(row.top11)} ${currency}`; age.innerText = `${mazyarFormatAverageAge(row.top11Age)}`; } else { top.innerText = `${mazyarFormatBigNumber(row.top21)} ${currency}`; age.innerText = `${mazyarFormatAverageAge(row.top21Age)}`; } tr.appendChild(title); tr.appendChild(count); tr.appendChild(top); tr.appendChild(age); tbody.appendChild(tr); } return tbody; } function matchCreateSummaryTable(rows, currency = "USD", sport = "soccer") { const table = document.createElement("table"); const thead = matchCreateTopPlayersHeader(sport); const tbody = matchCreateTopPlayersBody(rows, currency, sport); table.classList.add("mazyar-table", "tablesorter", "hitlist", "marker", "hitlist-compact-list-included"); table.style.marginBottom = "10px"; table.style.marginTop = "2em"; table.appendChild(thead); table.appendChild(tbody); return table; } function matchInjectTopPlayersValues(players, team, currency, sport) { const summary = mazyarExtractSquadSummaryDetails(players, sport); const table = matchCreateSummaryTable(summary, currency, sport); const div = document.createElement("div"); div.classList.add("mazyar-flexbox-column"); div.appendChild(table); team.appendChild(div); } // ---------------- Lineup -------------------- function matchGetLineupPlayers(teamNode, teamPlayers, sport) { const lineup = []; const teamPlayerIDs = teamPlayers.map((p) => p.id); const lineupPlayers = teamNode.querySelectorAll("tbody tr"); for (const playerNode of lineupPlayers) { const pos = playerNode.querySelector("td:nth-child(2)"); const order = Number(pos.querySelector("span").innerText); const pid = mazyarExtractPlayerIdFromProfileLink(playerNode.querySelector("a").href); const playerInfo = { id: pid, order, exPlayer: !teamPlayerIDs.includes(pid), starting: sport === "hockey" || order < 12, value: teamPlayers.find((p) => p.id === pid)?.value ?? 0, age: teamPlayers.find((p) => p.id === pid)?.age, }; const shirtNumber = playerNode.querySelector("td"); if (playerInfo.starting) { shirtNumber.style.background = "lightgreen"; } if (playerInfo.exPlayer) { shirtNumber.style.background = "#DD0000"; } const value = document.createElement("td"); value.innerText = `${playerInfo.value ? mazyarFormatBigNumber(playerInfo.value, ",") : "N/A"}`; playerNode.appendChild(value); const age = document.createElement("td"); age.innerText = `${playerInfo.age ?? "N/A"}`; playerNode.appendChild(age); lineup.push(playerInfo); } return lineup; } function matchInjectLineupValues(players, team, currency, sport) { const valueHeader = document.createElement("td"); valueHeader.innerText = `Value`; valueHeader.title = `Player Value (in ${currency})`; team.querySelector("table thead tr:nth-child(2)").appendChild(valueHeader); team.querySelector("table tfoot tr td").colSpan += 1; const ageHeader = document.createElement("td"); ageHeader.innerText = `Age`; ageHeader.title = `Player Age`; team.querySelector("table thead tr:nth-child(2)").appendChild(ageHeader); team.querySelector("table tfoot tr td").colSpan += 1; team.querySelector("table thead tr td").colSpan += 1; const lineupPlayers = matchGetLineupPlayers(team, players, sport).filter((player) => player.starting && !player.exPlayer); const value = lineupPlayers?.map((player) => player.value).reduce((a, b) => a + b, 0); let averageAge = lineupPlayers?.map((player) => player.age).reduce((a, b) => a + b, 0); if (averageAge) { averageAge /= lineupPlayers.length; } const div = document.createElement("div"); div.innerHTML = `${sport === "soccer" ? "Starting " : ""}Lineup` + `<br>Value: <b>${mazyarFormatBigNumber(value, ",")}</b> ${currency}` + `<br>Average Age: <b>${mazyarFormatAverageAge(averageAge)}</b>` + `<br><br>Note: <span style="background:lightgreen">YYY</span>` + ` are ${sport === "soccer" ? "starting " : "current"} players and ` + `<span style="background:#DD0000">NNN</span> are ex-players.` + `<br>ex-player's value is N/A and not included in Lineup Value calculation.`; div.style.margin = "10px"; div.style.padding = "5px"; div.style.border = "2px solid green"; div.style.borderRadius = "10px"; const place = team.querySelector("table"); team.insertBefore(div, place); } async function matchAddTopPlayersValue(team, sport) { const teamLink = team.querySelector("a").href; const tid = mazyarExtractTeamId(teamLink); const { players, currency } = await mazyarFetchPlayersAndCurrency(tid, sport); matchInjectTopPlayersValues(players, team, currency, sport); } async function matchAddLineupValues(team, sport) { const teamLink = team.querySelector("a").href; const tid = mazyarExtractTeamId(teamLink); const { players, currency } = await mazyarFetchPlayersAndCurrency(tid, sport); matchInjectLineupValues(players, team, currency, sport); } function matchInjectTeamValues() { const sport = mazyarExtractSportType(); const teams = document.querySelectorAll("div.team-table"); for (const team of teams) { if (team.querySelector("table")) { matchAddLineupValues(team, sport); } else { matchAddTopPlayersValue(team, sport); } } } /* ******************** In Progress Results ************************* */ function matchGetWinner(context) { const h = Number(context.homeGoals); const a = Number(context.awayGoals); if (h > a) { return context.homeId; } else if (h < a) { return context.awayId; } return null; } function matchUpdateResult(result, context) { result.innerText = context.homeGoals + " - " + context.awayGoals; if ([context.homeId, context.awayId].includes(context.targetId)) { const winnerId = matchGetWinner(context); if (winnerId) { result.style.background = winnerId === context.targetId ? "#5D7F13" : "#930000"; result.style.color = "#fff"; } else { // equal result.style.background = "#F2D624"; result.style.color = "#000"; } } else { result.style.background = "coral"; result.style.color = "#fff"; } result.classList.add("mazyar-in-progress-result-gray"); } function matchGetPossiblyInProgressMatches(section) { let days = 0; const possiblyInProgress = []; for (const child of [...section.children]) { const classes = [...child.classList]; if (classes?.includes("odd")) { if (!child.updated) { possiblyInProgress.push(child); } } else if (classes?.includes("group")) { days += 1; if (days == 3) { break; } } } return possiblyInProgress; } function matchWaitAndInjectInProgressResults(timeout = 32000) { const step = 500; const interval = setInterval(() => { const matchesSection = document.getElementById("fixtures-results-list"); if (matchesSection) { const games = matchesSection.querySelectorAll("dd.group"); if (games.length > 0) { matchAddInProgressResults(matchesSection); clearInterval(interval); } } else { timeout -= step; if (timeout < 0) { clearInterval(interval); } } }, step); } function matchAttachChangeEventToFilterForm() { const filterForm = document.getElementById("matchListForm"); if (filterForm && !filterForm.eventAttached) { filterForm.eventAttached = true; // Note: when this event is fired, it is removed from the element or element is changed somehow. // so for now we will attach it again after each change event. filterForm.addEventListener("change", matchWaitAndInjectInProgressResults); } } function matchAddInProgressResults(section) { matchAttachChangeEventToFilterForm(); const sport = mazyarExtractSportType(); const matches = matchGetPossiblyInProgressMatches(section); // when you visit someone else fixture, this return its id let teamId = mazyarExtractTeamId(document.baseURI); for (const match of matches) { const result = match.querySelector("dd.teams-wrapper a.score-shown"); if (!mazyarIsMatchInProgress(result.innerText)) { match.updated = true; continue; } const mid = mazyarExtractMatchId(result.href); // this always returns your id const visitorId = mazyarExtractTeamId(result.href); const url = `https://${location.hostname}/xml/match_info.php?sport_id=${sport === "soccer" ? 1 : 2}&match_id=${mid}`; mazyarFetchXml(url).then((xmlDoc) => { if (xmlDoc && !xmlDoc.querySelector("ManagerZone_Error")) { const home = xmlDoc.querySelector(`Team[field="home"]`); const away = xmlDoc.querySelector(`Team[field="away"]`); const context = { homeGoals: home.getAttribute("goals"), homeId: home.getAttribute("id"), awayGoals: away.getAttribute("goals"), awayId: away.getAttribute("id"), targetId: teamId ?? visitorId, }; matchUpdateResult(result, context); } match.updated = true; }); } } function matchInjectInProgressResults() { if (mazyar.mustDisplayInProgressResults()) { const fixturesLink = document.getElementById("matches_sub_nav")?.querySelector("div.flex-grow-0 span a"); if (fixturesLink) { fixturesLink.addEventListener("click", matchWaitAndInjectInProgressResults); if ([...fixturesLink.classList].includes("selected")) { const matchesSection = document.getElementById("fixtures-results-list"); if (matchesSection) { matchAddInProgressResults(matchesSection); } } } } } function fixtureCreateFullNameElement(name, tag) { const element = document.createElement(tag); element.classList.add("mazyar-text-truncate"); element.title = name; element.innerText = name; return element; } function fixtureChangeTeamNames(section) { section.querySelectorAll("dl.flex-wrap > dd.teams-wrapper.flex-grow-1").forEach((el) => { el.classList.add("mazyar-responsive-block"); }); section.querySelectorAll("dd.score-cell-wrapper").forEach((el) => { el.style.alignSelf = "center"; }); section.querySelectorAll("dd:is(.home-team-column, .away-team-column)").forEach((el) => { const fullName = el.querySelector("span.full-name"); const tag = (fullName?.parentNode.tagName === 'STRONG') ? "strong" : "span"; const name = fixtureCreateFullNameElement(fullName.innerText, tag); el.appendChild(document.createElement("br")); el.appendChild(name); }) } function fixtureChangeNames() { const callback = () => { const section = document.querySelector("dl#fixtures-results-list.fixtures"); if (section && !section.injecting) { section.injecting = true; fixtureChangeTeamNames(section); } }; callback(); const target = document.getElementById('fixtures-results-list-wrapper'); if (target) { const observer = new MutationObserver(callback); const config = { childList: true, subtree: true }; observer.observe(target, config); } } /* *********************** League ********************************** */ function tableGetAgeLimit(url) { if (url.search("type=u23") > -1) { return 23; } else if (url.search("type=u21") > -1) { return 21; } else if (url.search("type=u18") > -1) { return 18; } return 99; } function tableGetValeTitle(sport = "soccer", age = 99) { const count = sport === "soccer" ? 11 : 21; if ([18, 21, 23].includes(age)) { return `Top ${count} - U${age}`; } return `Top ${count} - All`; } function tableGetAgeClass(age = 99) { if ([18, 21, 23].includes(age)) { return `values-u${age}`; } return "values-all"; } function tableGetNextAge(age = 99) { if (age === 99) { return 23; } else if (age === 23) { return 21; } else if (age === 21) { return 18; } return 99; } function tableDisplayAgeInfo(team, ageLimit) { const target = team ?? document; target.querySelectorAll("td.mazyar-injected span")?.forEach((el) => { el.style.display = "none"; }); const ageClass = tableGetAgeClass(ageLimit); target.querySelectorAll("td.mazyar-injected span." + ageClass)?.forEach((el) => { el.style.display = "unset"; }); } function tableGetSquadSummaryUrl(team) { const teamLink = team.querySelector("td:nth-child(2) a:last-child")?.href; const tid = mazyarExtractTeamId(teamLink); return mazyarGetSquadSummaryUrl(tid); } function tableModifyTeamInBodyForPcView(team, url) { const teamValue = document.createElement("td"); team.appendChild(teamValue); const ageValue = document.createElement("td"); team.appendChild(ageValue); teamValue.replaceChildren(mazyarCreateLoadingIcon2()); teamValue.classList.add("mazyar-injected", "team-value"); teamValue.title = "Click to see squad summary"; teamValue.style.textAlign = "center"; teamValue.style.whiteSpace = "nowrap"; teamValue.style.padding = "auto 3px"; teamValue.onclick = () => { mazyar.displaySquadSummary(url); }; ageValue.replaceChildren(mazyarCreateLoadingIcon2()); ageValue.classList.add("mazyar-injected", "age-value"); ageValue.style.textAlign = "center"; ageValue.style.whiteSpace = "nowrap"; ageValue.style.padding = "auto 3px"; } function tableAddTeamToBodyForMobileView(team, url) { const firstRow = team.cloneNode(true); team.parentNode.insertBefore(firstRow, team.nextSibling); const secondRow = document.createElement("tr"); firstRow.parentNode.insertBefore(secondRow, firstRow.nextSibling); const value = document.createElement("td"); const age = document.createElement("td"); secondRow.appendChild(value); secondRow.appendChild(age); firstRow.children[0].rowSpan = "2"; firstRow.children[1].rowSpan = "2"; firstRow.classList.add("responsive-show", "mazyar-responsive-show"); secondRow.classList.add("responsive-show", "mazyar-responsive-show"); value.colSpan = "6"; value.replaceChildren(mazyarCreateLoadingIcon2()); value.classList.add("mazyar-injected", "team-value"); value.title = "Click to see squad summary"; value.style.textAlign = "center"; value.style.whiteSpace = "nowrap"; value.style.padding = "auto 3px"; value.style.backgroundColor = "aquamarine"; value.onclick = () => { mazyar.displaySquadSummary(url); }; age.colSpan = "2"; age.replaceChildren(mazyarCreateLoadingIcon2()); age.classList.add("mazyar-injected", "age-value"); age.style.textAlign = "center"; age.style.whiteSpace = "nowrap"; age.style.padding = "auto 3px"; age.style.backgroundColor = "aqua"; // modify borders to match the original view firstRow.style.borderBottom = "inherit"; secondRow.style.borderBottom = team.style.borderBottom; return secondRow; } async function tableAddTeamTopPlayersInfo(team, ageLimit, sport) { const url = tableGetSquadSummaryUrl(team); // for mobile const mobileView = tableAddTeamToBodyForMobileView(team, url); // for pc team.classList.add("responsive-hide"); tableModifyTeamInBodyForPcView(team, url); const pcView = team; const tid = mazyarExtractTeamId(url); const { players, currency } = await mazyarFetchPlayersAndCurrency(tid, sport); const playersOfSport = sport === "soccer" ? 11 : 21; const all = mazyarFilterPlayers(players, playersOfSport, 0, 99); const u23 = mazyarFilterPlayers(players, playersOfSport, 0, 23); const u21 = mazyarFilterPlayers(players, playersOfSport, 0, 21); const u18 = mazyarFilterPlayers(players, playersOfSport, 0, 18); for (const view of [pcView, mobileView]) { const valueElement = view.querySelector("td.team-value"); // prettier-ignore valueElement.innerHTML = `<span class="values-all" style="display:none">${mazyarFormatBigNumber(all?.values)} ${currency}</span>` + `<span class="values-u23" style="display:none">${mazyarFormatBigNumber(u23?.values)} ${currency}</span>` + `<span class="values-u21" style="display:none">${mazyarFormatBigNumber(u21?.values)} ${currency}</span>` + `<span class="values-u18" style="display:none">${mazyarFormatBigNumber(u18?.values)} ${currency}</span>`; valueElement.style.textAlign = "right"; const ageElement = view.querySelector("td.age-value"); // prettier-ignore ageElement.innerHTML = `<span class="values-all" style="display:none;">${mazyarFormatAverageAge(all?.avgAge)}</span>` + `<span class="values-u23" style="display:none;">${mazyarFormatAverageAge(u23?.avgAge)}</span>` + `<span class="values-u21" style="display:none;">${mazyarFormatAverageAge(u21?.avgAge)}</span>` + `<span class="values-u18" style="display:none;">${mazyarFormatAverageAge(u18?.avgAge)}</span>`; tableDisplayAgeInfo(view, ageLimit); } } function tableDisplayNextAgeInfo() { const headers = document.querySelectorAll("th.team-value-header"); let changed = false; for (const header of headers) { header.age = tableGetNextAge(header.age); header.innerText = tableGetValeTitle(header.sport, header.age); if (!changed) { changed = true; tableDisplayAgeInfo(null, header.age); } } } function tableModifyHeaderForPcView(header, sport = "soccer", ageLimit = 99) { const firstRow = header.querySelector("tr"); const value = document.createElement("th"); firstRow.appendChild(value); const age = document.createElement("th"); firstRow.appendChild(age); value.classList.add("responsive-hide", "team-value-header"); value.style.textAlign = "center"; value.innerText = tableGetValeTitle(sport, ageLimit); value.title = "Click to see other age limits"; value.age = ageLimit; value.sport = sport; value.onclick = tableDisplayNextAgeInfo; age.classList.add("responsive-hide"); age.style.textAlign = "center"; age.innerText = `Age`; age.title = "Average Age Of the Top Players"; } function tableAddHeaderForMobileView(mainHeader, sport = "soccer", ageLimit = 99) { const mobileHeader = mainHeader.cloneNode(true); mainHeader.parentNode.insertBefore(mobileHeader, mainHeader.nextSibling); const secondRow = document.createElement("tr"); mobileHeader.appendChild(secondRow); const value = document.createElement("th"); const age = document.createElement("th"); secondRow.appendChild(value); secondRow.appendChild(age); mobileHeader.classList.add("responsive-show", "mazyar-responsive-show"); const firstRow = mobileHeader.querySelector("tr"); firstRow.children[0].rowSpan = "2"; firstRow.children[1].rowSpan = "2"; value.classList.add("team-value-header"); value.colSpan = "6"; value.style.textAlign = "center"; value.innerText = tableGetValeTitle(sport, ageLimit); value.title = "Click to see other age limits"; value.age = ageLimit; value.sport = sport; value.onclick = tableDisplayNextAgeInfo; value.style.backgroundColor = "aquamarine"; age.colSpan = "2"; age.style.textAlign = "center"; age.innerText = `Age`; age.title = "Average Age Of the Top Players"; age.style.backgroundColor = "aqua"; } function tableInjectInProgressResults() { const sport = mazyarExtractSportType(); const matches = document.querySelectorAll("table.hitlist.marker td > a"); const inProgressMatches = [...matches].filter((match) => mazyarIsMatchInProgress(match.innerText)); for (const match of inProgressMatches) { const mid = mazyarExtractMatchId(match.href); const url = `https://${location.hostname}/xml/match_info.php?sport_id=${sport === "soccer" ? 1 : 2}&match_id=${mid}`; mazyarFetchXml(url).then((xmlDoc) => { if (xmlDoc && !xmlDoc.querySelector("ManagerZone_Error")) { const home = xmlDoc.querySelector(`Team[field="home"]`).getAttribute("goals"); const away = xmlDoc.querySelector(`Team[field="away"]`).getAttribute("goals"); match.innerText = home + " - " + away; match.classList.add("mazyar-in-progress-result-green"); } }); } } function tableAddTopPlayersInfo(table) { const ageLimit = tableGetAgeLimit(document.baseURI); const sport = mazyarExtractSportType(document); const mainHeader = table.querySelector("thead"); // for mobile tableAddHeaderForMobileView(mainHeader, sport, ageLimit); // for PC mainHeader.classList.add("responsive-hide"); tableModifyHeaderForPcView(mainHeader, sport, ageLimit); const teams = table.querySelectorAll("tbody tr"); for (const team of teams) { tableAddTeamTopPlayersInfo(team, ageLimit, sport); } } async function tableCreateTransferList(teams) { const sport = mazyarExtractSportType(); let rows = 0; const table = document.createElement("table"); table.classList.add("nice_table"); const thead = document.createElement("thead"); table.appendChild(thead); thead.innerHTML = ` <tr> <th>Team</th> <th>Player</th> <th>Age</th> <th>Deadline</th> <th>Fee</th> <th>Latest Bid</th> </tr>`; const tbody = document.createElement("tbody"); table.appendChild(tbody); for (const teamPlayers of teams) { for (const player of Object.keys(teamPlayers)) { if (teamPlayers[player].market) { rows += 1; const tr = document.createElement("tr"); tbody.appendChild(tr); tr.innerHTML = ` <td>${teamPlayers.team.outerHTML}</td> <td><a href="${teamPlayers[player]?.marketLink}">${teamPlayers[player]?.name}</a></td> <td>${teamPlayers[player]?.age}</td> <td class="mazyar-deadline">${mazyarCreateLoadingIcon2().outerHTML}</td> <td class="mazyar-fee">${mazyarCreateLoadingIcon2().outerHTML}</td> <td class="mazyar-bid">${mazyarCreateLoadingIcon2().outerHTML}</td>`; mazyarFetchPlayerMarketDetail(player, sport).then((detail) => { tr.querySelector(".mazyar-deadline").innerText = detail?.player?.deadlineFull ?? "passed"; tr.querySelector(".mazyar-fee").innerText = detail?.player?.fee ?? "n/a"; tr.querySelector(".mazyar-bid").innerText = detail?.player?.latestBid ?? "n/a"; }); } } } return rows > 0 ? table : document.createTextNode('No players in transfer market.'); } function getParentToAppendTransferSectionForLeagues(table) { if (table.parentNode.parentNode.id === 'tabs') { // friendly leagues return table.parentNode; } return table.parentNode.parentNode; } async function tableInjectTransferList(table) { const parent = getParentToAppendTransferSectionForLeagues(table); const header = document.createElement("h2"); header.classList.add("subheader", "clearfix"); header.style.marginTop = "5px"; header.innerText = "MZY Transfer List"; header.style.fontWeight = "bold"; parent?.appendChild(header); const div = document.createElement("div"); div.classList.add("mainContent"); div.style.padding = "5px"; div.innerText = "fetching..."; parent?.appendChild(div); const teams = table.querySelectorAll("tbody tr.responsive-hide"); const jobs = []; for (const team of teams) { const teamLink = team.querySelector("td:nth-child(2) a:last-child"); const tid = mazyarExtractTeamId(teamLink?.href); jobs.push(mazyarExtractPlayersProfileDetails(tid).then((detail) => { detail.team = teamLink; return detail; })); } const teamsPlayers = await Promise.all(jobs); const result = await tableCreateTransferList(teamsPlayers); div.replaceChildren(result); } function tableCreateTransferHistoryResultTable(histories, weeks) { let hasRows = false; const table = histories?.[0]?.cloneNode(2); table.classList.remove("marker"); table.classList.add("nice_table"); const [th3, th2] = table.querySelectorAll("thead tr td")[3].innerText.split("/"); table.querySelectorAll("thead tr td")[2].innerText = th2; table.querySelectorAll("thead tr td")[3].innerText = th3; const tbody = table?.querySelector("tbody"); tbody.replaceChildren(); histories.forEach((history) => { mazyarFilterTransferHistory(history, weeks); if (history.filterResults > 0) { tbody.append(...[...history.querySelectorAll("tbody tr")].map((el) => el.cloneNode(2))); hasRows = true; } }); return hasRows ? table : document.createTextNode("No history to display"); } async function tableInjectRecentTransferHistory(table) { const parent = getParentToAppendTransferSectionForLeagues(table); const header = document.createElement("div"); header.classList.add("mazyar-flexbox-row", "subheader", "clearfix"); header.style.justifyContent = "space-between"; header.style.marginTop = "5px"; const headerTitle = document.createElement("div"); headerTitle.innerText = "MZY Transfer History"; headerTitle.style.fontWeight = "bold"; headerTitle.style.margin = "3px 0px"; const options = { 1: { value: "1", label: "last week", }, 2: { value: "2", label: "last two weeks", }, 3: { value: "3", label: "last three weeks", }, 4: { value: "4", label: "last month", }, } const weekSelector = mazyarCreateDropDownMenu("", options, options[4].value); weekSelector.style.alignSelf = "unset"; weekSelector.style.margin = "unset"; weekSelector.style.marginLeft = "auto"; header.appendChild(headerTitle); parent?.appendChild(header); const div = document.createElement("div"); div.classList.add("mainContent"); div.style.padding = "5px"; div.innerText = "fetching..."; parent?.appendChild(div); const histories = await mazyarGetTableTransferHistories(table); // now is the time to attach the selector header.appendChild(weekSelector); // delete histories older than maximum period const maxWeeks = Math.max(...Object.keys(options)); histories.forEach((history) => mazyarRemoveOldTransferHistory(history, maxWeeks)); const selectedWeeks = weekSelector.querySelector("select"); const result = tableCreateTransferHistoryResultTable(histories, parseInt(selectedWeeks.value)); div.replaceChildren(result); selectedWeeks.addEventListener("input", () => { const result = tableCreateTransferHistoryResultTable(histories, parseInt(selectedWeeks.value)); div.replaceChildren(result); }) } function tableWaitAndInjectTopPlayersInfo(timeout = 16000) { const step = 500; const interval = setInterval(() => { const table = document.querySelector("table.nice_table"); if (table) { clearInterval(interval); if (!table.TopPlayersInfoInjected) { table.TopPlayersInfoInjected = true; if (mazyar.mustDisplayTopPlayersInTables()) { tableAddTopPlayersInfo(table); } if (mazyar.mustDisplayInProgressResults()) { tableInjectInProgressResults(); } if (mazyar.mustAddTransferListToTable()) { // owned by factual-insights } if (mazyar.mustAddTransferHistoryToTable()) { // owned by factual-insights } } } else { timeout -= step; if (timeout < 0) { clearInterval(interval); } } }, step); } function tableInjectTopPlayersToOfficialLeague() { // default sub-page (or tab) for leagues is Table. so try to inject team value after table is loaded tableWaitAndInjectTopPlayersInfo(); // also add 'onclick' handler to Table tab const links = document.getElementsByTagName("a"); for (const link of links) { if (["p=league", "sub=table"].every((text) => link.href.indexOf(text) > -1)) { link.onclick = tableWaitAndInjectTopPlayersInfo; } } } function tableInjectTopPlayersInfoToFriendlyLeague() { tableWaitAndInjectTopPlayersInfo(); const links = document.getElementsByTagName("a"); for (const link of links) { if (["p=friendlySeries", "sub=standings"].every((text) => link.href.indexOf(text) > -1)) { link.onclick = tableWaitAndInjectTopPlayersInfo; } } } function tableInjectTopPlayersInfoToCup() { tableWaitAndInjectTopPlayersInfo(); const links = document.getElementsByTagName("a"); for (const link of links) { if (["p=cups", "sub=groupplay"].every((text) => link.href.indexOf(text) > -1)) { link.onclick = tableWaitAndInjectTopPlayersInfo; } } } /* *********************** Schedule ********************************** */ function scheduleClearAllColorings(teams) { teams.forEach((team) => { team.style.backgroundColor = team.originalColor; }); } function scheduleResultColors(result) { if (result.length < 2) { return ["cyan", "cyan"]; } if (result[0] === "X") { return ["cyan", "cyan"]; } if (result[0] < result[1]) { return ["orangered", "lime"]; } else if (result[0] > result[1]) { return ["lime", "orangered"]; } return ["yellow", "yellow"]; } function scheduleColorizeThisTeam(teams, selected) { teams.forEach((team) => { if (team.innerText === selected) { const tr = team.parentNode; const opponents = tr.querySelectorAll("td:nth-child(odd)"); const result = tr.querySelector("td:nth-child(2)").innerText.split(" - "); const colors = scheduleResultColors(result); if (opponents[0].innerText === selected) { opponents[0].style.setProperty("background-color", colors[0], "important"); } if (opponents[1].innerText === selected) { opponents[1].style.setProperty("background-color", colors[1], "important"); } } }); } function scheduleInjectColoring(tab) { let selected = ""; const teams = tab.querySelectorAll("div.mainContent td:nth-child(odd)"); for (const team of teams) { team.originalColor = team.style.backgroundColor; team.onclick = null; team.addEventListener("click", function (evt) { if (selected && selected !== this.innerText) { // new team is selected scheduleClearAllColorings(teams); selected = ""; } if (selected === this.innerText) { // de-colorize selected = ""; scheduleClearAllColorings(teams); } else { // colorize selected = this.innerText; scheduleColorizeThisTeam(teams, selected); } }); } } function scheduleHasDuplicateName(round) { const teams = round.querySelectorAll("td:nth-child(odd)"); const names = [...teams].map((t) => t.innerText); return mazyarHasDuplicates(names); } function scheduleWaitAndInjectColoring(timeout = 16000) { const step = 500; const interval = setInterval(() => { const firstRound = document.querySelector("div[aria-labelledby='league_tab_schedule'] div.mainContent"); if (firstRound) { const schedule = firstRound.parentNode; if (scheduleHasDuplicateName(firstRound)) { const note = document.createElement("p"); note.innerHTML = `<b style="color: red;">Note: </b><span>Some teams have similar names. Coloring will not work as expected.</span>`; note.style.fontSize = "1.2em"; schedule.insertBefore(note, schedule.firstChild); } clearInterval(interval); scheduleInjectColoring(schedule); } else { timeout -= step; if (timeout < 0) { clearInterval(interval); } } }, step); } function scheduleInjectColoringToOfficialLeague() { const link = document.getElementById("league_tab_schedule"); if (link) { const tab = link.parentNode; if (!tab.coloringInjected) { tab.coloringInjected = true; tab.onclick = scheduleWaitAndInjectColoring; } } } /* *********************** Transfer Agent ********************************** */ function hasPlayerScoutReport(player) { return !!player.querySelector("tr.scout_report_row.box_dark"); } function transferGetInputValue(el) { if (el.type == "checkbox") { return el.checked ? 1 : 0; } return el.value; } function transferGetSearchParams() { const inputs = document.getElementById("searchform")?.querySelectorAll("input,select"); return [...inputs].map((el) => `&${el.name}=${transferGetInputValue(el)}`).join(""); } function transferGetFilterNameFromUrl() { const url = new URL(document.URL); const params = new URLSearchParams(url.search); if (params.has("mzy_filter_name")) { return params.get("mzy_filter_name"); } return null; } function transferLoadFilter(filterParams) { const url = new URL(`https://${location.hostname}/?${filterParams}`); const params = new URLSearchParams(url.search); for (const param of params) { const element = document.getElementById(param[0]); if (element) { if (element.type === "text") { element.value = param[1]; element.dispatchEvent(new Event("blur")); } else if (element.type === "checkbox") { element.checked = param[1] === "1"; } else if (element.tagName === "SELECT") { element.value = param[1]; } } } } function transferInjectButtons(form) { const target = form.querySelector("div.buttons-wrapper.clearfix"); if (target) { const saveButton = mazyarCreateMzStyledButton("MZY Save Filter", "blue", "floatLeft"); saveButton.style.margin = "0"; saveButton.onclick = () => { const params = transferGetSearchParams(); mazyar.displayFilterSaveMenu(params); }; target.appendChild(saveButton); } } function transferCreateScoutOptions() { const div = document.createElement("div"); div.classList.add("mazyar-flexbox-row"); div.style.justifyContent = "left"; div.style.marginTop = "6px"; const highs = document.createElement("div"); highs.classList.add("mazyar-flexbox-row"); highs.style.justifyContent = "left"; highs.style.border = "1px inset black"; highs.style.marginRight = "2rem"; const lows = document.createElement("div"); lows.classList.add("mazyar-flexbox-row"); lows.style.justifyContent = "left"; lows.style.border = "1px outset black"; lows.style.marginRight = "2rem"; const transferOptions = mazyar.getTransferOptions(); const H4 = mazyarCreateMenuCheckBox("H4", transferOptions.H4, { margin: "auto 4px auto 1px" }); const H3 = mazyarCreateMenuCheckBox("H3", transferOptions.H3, { margin: "auto 4px auto 1px" }); const L2 = mazyarCreateMenuCheckBox("L2", transferOptions.L2, { margin: "auto 4px auto 1px" }); const L1 = mazyarCreateMenuCheckBox("L1", transferOptions.L1, { margin: "auto 4px auto 1px" }); const hide = mazyarCreateMenuCheckBox("Hide Others", transferOptions.hide, { margin: "auto 4px auto 1px" }); H4.onclick = () => { mazyar.updateTransferOptions("H4", H4.querySelector("input[type=checkbox]").checked); mazyar.updateDisplayForTransferSearchResults(true); }; H3.onclick = () => { mazyar.updateTransferOptions("H3", H3.querySelector("input[type=checkbox]").checked); mazyar.updateDisplayForTransferSearchResults(true); }; L2.onclick = () => { mazyar.updateTransferOptions("L2", L2.querySelector("input[type=checkbox]").checked); mazyar.updateDisplayForTransferSearchResults(true); }; L1.onclick = () => { mazyar.updateTransferOptions("L1", L1.querySelector("input[type=checkbox]").checked); mazyar.updateDisplayForTransferSearchResults(true); }; hide.onclick = () => { mazyar.updateTransferOptions("hide", hide.querySelector("input[type=checkbox]").checked); mazyar.updateDisplayForTransferSearchResults(true); }; highs.appendChild(H4); highs.appendChild(H3); lows.appendChild(L2); lows.appendChild(L1); div.appendChild(highs); div.appendChild(lows); div.appendChild(hide); div.appendChild(hide); return div; } function transferInject() { const form = document.getElementById("searchform"); if (form) { const scoutOptions = transferCreateScoutOptions(); form.parentNode.appendChild(scoutOptions); transferInjectButtons(form); const filterName = transferGetFilterNameFromUrl(); if (filterName) { const filterParams = mazyar.getFilterParams(filterName); if (filterParams) { setTimeout(() => { transferLoadFilter(filterParams); document.getElementById("searchb")?.click(); }, 5000); } } } } /* *********************** Predictor ********************************** */ async function getNationalRankings() { const url = `https://${location.hostname}/?p=rank&sub=countryrank`; const doc = await mazyarFetchHtml(url); const rankings = []; if (doc) { const teams = doc.querySelectorAll("table#countryRankTable tbody tr"); for (const team of teams) { const columns = team.querySelectorAll("td"); let name = columns[2].innerText; if (name === 'United States') { name = 'USA'; } rankings.push({ name, rank: Number(columns[0].innerText), }) } } return rankings; } function predictorAttachInfo(match, teams, rankings) { teams[0].rank = rankings.find((t) => t.name.trim() === teams[0].name.trim())?.rank; teams[1].rank = rankings.find((t) => t.name.trim() === teams[1].name.trim())?.rank; teams[0].valueColor = (teams[0].value > teams[1].value) ? "blue" : "unset"; teams[1].valueColor = (teams[1].value > teams[0].value) ? "blue" : "unset"; teams[0].rankColor = (teams[0].rank < teams[1].rank) ? "blue" : "unset"; teams[1].rankColor = (teams[1].rank < teams[0].rank) ? "blue" : "unset"; const tbody = document.createElement("tbody"); for (const team of teams) { tbody.innerHTML += ` <td style="text-align:left;padding: 0; width: 100px;"><b>${team.name}</b></td> <td style="text-align:left;padding: 0;">[Rank: <b style="display:inline-block; width: 1.1rem; text-align:right; color: ${team.rankColor};">${team.rank ?? 'xx'}</b>]</td> <td style="text-align:right;padding: 0 4px;">[Value: <b style="color: ${team.valueColor};">${mazyarFormatBigNumber(team.value)}</b> ${team.currency}]</td> `; } const table = document.createElement("table"); table.appendChild(tbody); table.classList.add("mazyar-table"); const div = document.createElement("div"); div.appendChild(table); const target = match.querySelector("td"); target.appendChild(div); } async function predictorInjectRecommendation(match, rankings) { const href = match.querySelector("td > a")?.href; const url = href.replace("&play=2d", "").replace("&type=2d", ""); const sport = mazyarExtractSportType(); const doc = await mazyarFetchHtml(url); if (doc) { const teams = doc.querySelectorAll("div.team-table"); const results = []; for (const team of teams) { const link = team.querySelector("a"); const name = link.innerText; const tid = mazyarExtractTeamId(link.href); const { players, currency } = await mazyarFetchPlayersAndCurrency(tid, sport); const playersOfSport = sport === "soccer" ? 11 : 21; const all = mazyarFilterPlayers(players, playersOfSport, 0, 99); results.push({ name, value: all?.values, currency: currency, }) } predictorAttachInfo(match, results, rankings); } } async function predictorInject() { const rankings = await getNationalRankings(); const matches = document.querySelectorAll("#match-predictor-container table.match-list tbody tr"); const jobs = []; for (const match of matches) { jobs.push(predictorInjectRecommendation(match, rankings)); } Promise.all(jobs); } /* *********************** Club Page ********************************** */ async function getTrophiesCount(url) { const doc = await mazyarFetchHtml(url); if (doc) { const trophies = doc.querySelectorAll("div.trophy-wrapper:not(.icon)"); return [...trophies].map((el) => { const text = el.innerText.trim(); return text ? Number(text) : 1; }).reduce((a, b) => a + b, 0); } return 'N/A' } function getTrophiesSection() { const items = document.querySelectorAll("dl#trophies-achievements-lists dd"); for (const item of items) { const link = item.querySelector("a"); if (link?.href.search("awards") > -1) { return { trophies: item, url: link.href }; } } return { trophies: null, url: null }; } async function addTrophyCountToClubPage() { const { trophies, url } = getTrophiesSection(); if (trophies) { const el = document.createTextNode(` (Total: ?)`); trophies.querySelector("span")?.appendChild(el); const count = await getTrophiesCount(url); el.nodeValue = ` (Total: ${count})`; } } /* *********************** Trainers ********************************** */ function trainersAddColumns(table) { const headerRow = table.querySelector('thead tr'); const thSalary = document.createElement('th'); thSalary.textContent = 'Salary Range'; thSalary.style.textAlign = 'center'; thSalary.style.textDecoration = 'none'; headerRow?.appendChild(thSalary); const thBonus = document.createElement('th'); thBonus.textContent = 'Bonus'; thBonus.style.textAlign = 'center'; thBonus.style.textDecoration = 'none'; headerRow?.appendChild(thBonus); const thWeeks = document.createElement('th'); thWeeks.textContent = 'Weeks'; thWeeks.style.textAlign = 'center'; thWeeks.style.textDecoration = 'none'; headerRow?.appendChild(thWeeks); const rows = table.querySelectorAll('tbody tr:not(.minified-view)'); rows.forEach(row => { const salaryCell = row.insertCell(-1); salaryCell.style.textAlign = 'center'; salaryCell.replaceChildren(mazyarCreateLoadingIcon2()); const bonusCell = row.insertCell(-1); bonusCell.style.textAlign = 'center'; bonusCell.replaceChildren(mazyarCreateLoadingIcon2()); const weeksCell = row.insertCell(-1); weeksCell.style.textAlign = 'center'; weeksCell.replaceChildren(mazyarCreateLoadingIcon2()); }); } async function trainersFetchSalaryAndWeeks(coachId, salaryCell, bonusCell, weeksCell) { const url = `https://${location.hostname}/?p=trainers&sub=offer&extra=freeagent&cid=${coachId}`; const doc = await mazyarFetchHtml(url); const info = {}; if (doc) { info.salary = doc.querySelector('td#salary_range nobr')?.innerText?.trim()?.match(/\d+(.*?\d+)* -(.*?\d+)+/g)?.[0]; info.weeks = doc.querySelector('td#weeks_range nobr')?.innerText?.trim()?.match(/\d+(.*?\d+)* -(.*?\d+)+/g)?.[0]; info.bonus = doc.querySelector("div#paper-content-wrapper > table td.contract_paper:nth-child(2)")?.innerText?.trim()?.match(/\d+(.*?\d+)*/g)?.[0]; }; salaryCell.textContent = info.salary ?? 'N/A'; salaryCell.style.fontStyle = 'normal'; weeksCell.textContent = info.weeks ?? 'N/A'; weeksCell.style.fontStyle = 'normal'; bonusCell.textContent = info.bonus ?? 'N/A'; bonusCell.style.fontStyle = 'normal'; } function trainersUpdateSalariesAndWeeks(table) { trainersAddColumns(table); const rows = table?.querySelectorAll('tbody tr:not(.minified-view)'); rows.forEach(row => { const linkElement = row.querySelector('td a[href*="cid="]'); if (linkElement) { const urlParams = new URLSearchParams(linkElement.search); const coachId = urlParams.get('cid'); const salaryCell = row.cells[row.cells.length - 3]; const bonusCell = row.cells[row.cells.length - 2]; const weeksCell = row.cells[row.cells.length - 1]; trainersFetchSalaryAndWeeks(coachId, salaryCell, bonusCell, weeksCell); } }); } function trainersAddRequestedSalaries() { const callback = () => { const table = document.getElementById("coaches_list"); if (table && !table.injecting) { table.injecting = true; trainersUpdateSalariesAndWeeks(table); } }; const target = document.querySelector('div.in_page_navigation_top'); if (target) { const observer = new MutationObserver(callback); const config = { childList: true, subtree: true }; observer.observe(target, config); } } /* *********************** Tactic ********************************** */ function tacticAddDays() { const cssSelector = '#lightboxContent_player_profile #players_container'; const callback = () => { const profile = document.querySelector(cssSelector); if (profile && !profile.daysInjected) { profile.daysInjected = true; mazyar.trainingAddDaysAtThisClubToPlayerProfile(profile); mazyar.addPlayerComment(); } } const target = document.body; const config = { childList: true, subtree: true }; const observer = new MutationObserver(callback); observer.observe(target, config); } /* *********************** Training Report ********************************** */ function trainingOpenPlayerTrainingCamp(player) { player.querySelector('td.playerColumn a')?.click(); const interval = setInterval(() => { const modal = document.querySelector("#lightbox_player_profile #players_container div.playerContainer"); if (modal) { clearInterval(interval); modal.querySelector('div.box_dark.player_tc_package_information a')?.click(); } }, 300); } async function trainingInjectCampOpener(table) { const column = 6; const head = table.querySelector("thead tr"); const th = document.createElement("td"); th.style.textAlign = "right"; th.innerText = "Left"; head.insertBefore(th, head.children[column]); const players = table.querySelectorAll("tbody tr"); for (const player of players) { const pid = mazyarExtractPlayerIdFromProfileLink(player.querySelector(".player_link")?.href); const campIcon = player.querySelector("td.dailyReportRightColumn img.content_middle"); const td = document.createElement("td"); td.style.textAlign = "right"; if (campIcon) { campIcon.style.cursor = "pointer"; campIcon.addEventListener("click", () => { trainingOpenPlayerTrainingCamp(player); }); td.innerHTML = `<strong id="mazyar-pid-${pid}"></strong>`; } player.insertBefore(td, player.children[column]); } mazyarFetchTrainingCampDetail().then((detail) => { if (detail) { for (const pid of Object.keys(detail)) { const player = table.querySelector(`#mazyar-pid-${pid}`); if (player) { player.innerText = detail[pid].days ?? "n/a"; } } } }) } function trainingAddCampOpenerToReport() { const callback = () => { const table = document.querySelector('#training_report > table'); if (!table.injecting) { table.injecting = true; trainingInjectCampOpener(table); } }; const target = document.getElementById('training_report'); if (target) { const observer = new MutationObserver(callback); const config = { childList: true, subtree: true }; observer.observe(target, config); } } function trainingAddDays() { const cssSelector = '#lightboxContent_player_profile #players_container'; const callback = () => { const profile = document.querySelector(cssSelector); if (profile && !profile.daysInjected) { profile.daysInjected = true; mazyar.trainingAddDaysAtThisClubToPlayerProfile(profile); mazyar.addPlayerComment(); } } const target = document.body; const config = { childList: true, subtree: true }; const observer = new MutationObserver(callback); observer.observe(target, config); } /* *********************** Shortlist ********************************** */ function shortlistAddDays() { const container = document.querySelector('div#shortlist_window div#players_container'); mazyar.addDaysAtThisClubToAllPlayers(container); mazyar.addPlayerComment(); } /* *********************** Class ********************************** */ class Mazyar { #notebook = { text: "", style: { hide: true, top: 0, left: 0, width: 200, height: 250, } } #settings = { miscellaneous: { in_progress_results: true, fixture_full_name: false, top_players_in_tables: true, mz_predictor: false, player_comment: false, coach_salary: false, table_transfer_list: false, table_transfer_history: false, }, transfer: { enable_filters: false, display_maxed: false, display_camp: false, deadline: { enabled: false, play_bell: false, timeout: 30,// minutes, }, }, days: { display_in_profiles: false, display_in_transfer: false, display_in_training: false, display_for_one_clubs: false, }, }; #transferOptions = { hide: true, H4: false, H3: false, L2: false, L1: false, }; #filters = { soccer: [], hockey: [] }; // each key is like [{id, name, params, scout, interval}] #sport = "soccer"; #deadlines = {}; // {pid1: {name, deadline}, ...} #db; #deadlineLockAcquired; #updateInfo = { version: '', lastCheck: 0 }; constructor(db) { this.#db = db; this.#fetchSettings(); this.#fetchTransferOptions(); this.#fetchFilters(); this.#fetchUpdateInfo(); this.#sport = mazyarExtractSportType(); this.#createNotebook(); this.#addToolbar(); this.#createModal(); this.#showChangelog(); if (!this.isTransferFiltersEnabled()) { this.#resetTransferOptions(); } } // -------------------------------- Update ------------------------------------- #fetchUpdateInfo() { this.#updateInfo = GM_getValue("update_info", { version: CURRENT_VERSION, lastCheck: Date.now() }); } #saveUpdateInfo() { GM_setValue("update_info", this.#updateInfo); } async #checkForUpdate(menu) { const current = this.#getVersionNumbers(CURRENT_VERSION); if (Date.now() - this.#updateInfo.lastCheck > (UPDATE_CHECK_INTERVAL_MINUTES * 60 * 10000)) { this.#updateInfo.lastCheck = Date.now(); const url = GM_info.script.updateURL ?? "https://update.greasyfork.org/scripts/476290/Mazyar.meta.js"; const timestamp = "?" + new Date().getTime(); await mazyarFetchHtmlWithGM(url + timestamp, 7).then((doc) => { const text = doc?.body.innerText ?? ''; const matched = RegExp(/@version\s+(\d+\.\d+)/).exec(text); if (matched?.[1]) { const latest = this.#getVersionNumbers(matched?.[1]); if (this.#isVersionGreaterThan(latest, current)) { this.#updateInfo.version = matched?.[1]; } else { this.#updateInfo.version = null; } } }); this.#saveUpdateInfo(); } if (this.#updateInfo.version) { const latest = this.#getVersionNumbers(this.#updateInfo.version); if (this.#isVersionGreaterThan(latest, current)) { menu.classList.add("mazyar-update-available"); } else { this.#updateInfo.version = null; this.#updateInfo.lastCheck = Date.now(); this.#saveUpdateInfo(); } } } // -------------------------------- Settings ------------------------------------- #fetchSettings() { this.#settings.transfer.enable_filters = GM_getValue("enable_transfer_filters", true); this.#settings.transfer.display_maxed = GM_getValue("display_maxed_in_transfer", true); this.#settings.transfer.display_camp = GM_getValue("display_camp_in_transfer", true); this.#settings.transfer.deadline.enabled = GM_getValue("deadline", true); this.#settings.transfer.deadline.play_bell = GM_getValue("deadline_play_bell", false); this.#settings.transfer.deadline.timeout = GM_getValue("deadline_timeout", 30); this.#settings.days.display_in_profiles = GM_getValue("display_days_in_profiles", true); this.#settings.days.display_in_transfer = GM_getValue("display_days_in_transfer", false); this.#settings.days.display_in_training = GM_getValue("display_days_in_training", true); this.#settings.days.display_for_one_clubs = GM_getValue("display_days_for_one_clubs", true); this.#settings.miscellaneous.in_progress_results = false; this.#settings.miscellaneous.top_players_in_tables = false; this.#settings.miscellaneous.mz_predictor = GM_getValue("mz_predictor", false); this.#settings.miscellaneous.player_comment = GM_getValue("player_comment", true); this.#settings.miscellaneous.coach_salary = GM_getValue("coach_salary", true); this.#settings.miscellaneous.fixture_full_name = GM_getValue("fixture_full_name", true); this.#settings.miscellaneous.table_transfer_list = GM_getValue("table_transfer_list", true); this.#settings.miscellaneous.table_transfer_history = GM_getValue("table_transfer_history", true); } #saveMiscellaneousSettings() { GM_setValue("display_in_progress_results", this.#settings.miscellaneous.in_progress_results); GM_setValue("display_top_players_in_tables", this.#settings.miscellaneous.top_players_in_tables); GM_setValue("mz_predictor", this.#settings.miscellaneous.mz_predictor); GM_setValue("player_comment", this.#settings.miscellaneous.player_comment); GM_setValue("coach_salary", this.#settings.miscellaneous.coach_salary); GM_setValue("fixture_full_name", this.#settings.miscellaneous.fixture_full_name); GM_setValue("table_transfer_list", this.#settings.miscellaneous.table_transfer_list); GM_setValue("table_transfer_history", this.#settings.miscellaneous.table_transfer_history); document.getElementById("mazyar-in-progress-icon").style.color = this.mustDisplayInProgressResults() ? "greenyellow" : "unset"; } #saveTransferSettings() { GM_setValue("enable_transfer_filters", this.#settings.transfer.enable_filters); GM_setValue("display_maxed_in_transfer", this.#settings.transfer.display_maxed); GM_setValue("display_camp_in_transfer", this.#settings.transfer.display_camp); GM_setValue("deadline", this.#settings.transfer.deadline.enabled); GM_setValue("deadline_play_bell", this.#settings.transfer.deadline.play_bell); GM_setValue("deadline_timeout", this.#settings.transfer.deadline.timeout); } #saveDaysSettings() { GM_setValue("display_days_in_profiles", this.#settings.days.display_in_profiles); GM_setValue("display_days_in_transfer", this.#settings.days.display_in_transfer); GM_setValue("display_days_in_training", this.#settings.days.display_in_training); GM_setValue("display_days_for_one_clubs", this.#settings.days.display_for_one_clubs); } #updateTransferSettings(settings) { this.#settings.transfer = JSON.parse(JSON.stringify(settings)); this.#saveTransferSettings(); this.#resetTransferOptions(); if (this.isTransferFiltersEnabled()) { this.#checkAllFilters(true); } else { this.#setFilterHitsInToolbar(0); } } #updateDaysSettings(settings) { this.#settings.days = JSON.parse(JSON.stringify(settings)); this.#saveDaysSettings(); } #updateMiscellaneousSettings(settings) { this.#settings.miscellaneous = JSON.parse(JSON.stringify(settings)); this.#saveMiscellaneousSettings(); } #disableDisplayingInProgressResults() { this.#settings.miscellaneous.in_progress_results = false; GM_setValue("display_in_progress_results", this.#settings.miscellaneous.in_progress_results); } #enableDisplayingInProgressResults() { this.#settings.miscellaneous.in_progress_results = true; GM_setValue("display_in_progress_results", this.#settings.miscellaneous.in_progress_results); } async #cleanInstall() { this.#updateMiscellaneousSettings({ in_progress_results: false, top_players_in_tables: false, mz_predictor: false, player_comment: false, coach_salary: false, fixture_full_name: false, table_transfer_list: false, table_transfer_history: false, }); this.#updateDaysSettings({ display_in_profiles: false, display_in_transfer: false, display_in_training: false, display_for_one_clubs: false, }); this.#updateTransferSettings({ enable_filters: false, display_maxed: false, display_camp: false, deadline: { enabled: false, play_bell: false, timeout: 30,// minutes, }, }); this.deleteAllFilters(); await this.#clearIndexedDb(); this.#resetTransferOptions(); } // -------------------------------- Transfer Options ------------------------------------- #fetchTransferOptions() { this.#transferOptions.hide = GM_getValue("transfer_options_hide", true); this.#transferOptions.H4 = GM_getValue("transfer_options_H4", false); this.#transferOptions.H3 = GM_getValue("transfer_options_H3", false); this.#transferOptions.L2 = GM_getValue("transfer_options_L2", false); this.#transferOptions.L1 = GM_getValue("transfer_options_L1", false); } #saveTransferOptions() { GM_setValue("transfer_options_hide", this.#transferOptions.hide); GM_setValue("transfer_options_H4", this.#transferOptions.H4); GM_setValue("transfer_options_H3", this.#transferOptions.H3); GM_setValue("transfer_options_L2", this.#transferOptions.L2); GM_setValue("transfer_options_L1", this.#transferOptions.L1); } #transferOptionsMustHide() { return this.#transferOptions.hide; } getTransferOptions() { return this.#transferOptions; } updateTransferOptions(key, value) { this.#transferOptions[key] = value; this.#saveTransferOptions(); } #resetTransferOptions() { this.#transferOptions.hide = false; this.#transferOptions.H4 = false; this.#transferOptions.H3 = false; this.#transferOptions.L2 = false; this.#transferOptions.L1 = false; this.#saveTransferOptions(); } // -------------------------------- Settings Getters ------------------------------------- isTransferFiltersEnabled() { return this.#settings.transfer.enable_filters; } #mustMarkMaxedSkills() { return this.#settings.transfer.display_maxed; } #isTransferCampEnabled() { return this.#settings.transfer.display_camp; } #isTransferDeadlineAlertEnabled() { return this.#settings.transfer.deadline.enabled; } #getTransferDeadlineTimeout() { return this.#settings.transfer.deadline.timeout } #mustPlayBellForDeadline() { return this.#settings.transfer.deadline.play_bell; } isDaysAtThisClubEnabledForPlayerProfiles() { return this.#settings.days.display_in_profiles; } #isDaysAtThisClubEnabledForTraining() { return this.#settings.days.display_in_training; } #isDaysAtThisClubEnabledForTransferMarket() { return this.#settings.days.display_in_transfer; } isDaysAtThisClubEnabledForOneClubPlayers() { return this.#settings.days.display_for_one_clubs; } mustDisplayInProgressResults() { return this.#settings.miscellaneous.in_progress_results; } mustDisplayTopPlayersInTables() { return this.#settings.miscellaneous.top_players_in_tables; } mustHelpWithPredictor() { return this.#settings.miscellaneous.mz_predictor; } #mustAddPlayerComment() { return this.#settings.miscellaneous.player_comment; } mustAddCoachSalaries() { return this.#settings.miscellaneous.coach_salary; } mustAddFullNamesToFixture() { return this.#settings.miscellaneous.fixture_full_name; } mustAddTransferListToTable() { return this.#settings.miscellaneous.table_transfer_list; } mustAddTransferHistoryToTable() { return this.#settings.miscellaneous.table_transfer_history; } // -------------------------------- Database ------------------------------------- async #clearIndexedDb() { // TODO: clear comments? await this.#db.scout.clear(); await this.#db.hit.clear(); await this.#db.filter.clear(); await this.#db.player.clear(); await this.#db.hide.clear(); await this.#db.deadline.clear(); } async #isPlayerInHideListInIndexDb(pid) { const player = await this.#db.hide.get({ pid, sport: this.#sport }); return player?.ts > 0; } async #addPlayerToHideListInIndexDb(pid) { if (pid) { await this.#db.hide.put({ pid, sport: this.#sport, ts: Date.now() }); } } async #addPlayerToDeadlineListInIndexDb(player = { pid: "", deadline: 0, name: "", deadlineFull: "", latestBid: "", source: "monitor", flag: "", }) { if (player?.pid) { this.#deadlines[player.pid] = { ...player }; await this.#db.deadline.put({ sport: this.#sport, ts: Date.now(), pid: player.pid, deadline: player.deadline, name: player.name, deadlineFull: player.deadlineFull, latestBid: player.latestBid, source: player.source, flag: player.flag, }); } } async #removePlayerFromDeadlineList(pid) { if (pid) { delete this.#deadlines[pid]; await this.#db.deadline.delete([this.#sport, pid]) .catch((err) => console.warn(err)); } } async #fetchDeadlinePlayersFromIndexedDb() { if (!this.#isTransferDeadlineAlertEnabled()) { return []; } return await this.#db.deadline.toArray() .then((players) => { return players?.filter((player) => player.sport === this.#sport); } ).catch((err) => { console.warn(err); return []; }); } async #deletePlayersFromHideListInIndexDb(days = 0) { const outdate = Date.now() - days * 24 * 60 * 60 * 1000; return await this.#db.hide.where("ts").below(outdate).delete().then((deleteCount) => { return deleteCount; }).catch((error) => { console.warn(error); return 0; }); } async #countPlayersOfHideListInIndexDb() { return await this.#db.hide.where("ts").above(0).count(); } async #fetchPlayerCommentFromIndexedDb(pid) { const player = await this.#db.comment.get({ pid, sport: this.#sport }); return player?.comment ?? ""; } async #setPlayerCommentInIndexedDb(pid, comment, name = null) { if (pid && comment) { if (name) { await this.#db.comment.put({ pid, sport: this.#sport, comment, name }); } else { await this.#db.comment.update({ pid, sport: this.#sport }, { comment }); } } } async #updatePlayerNameForCommentInIndexedDb(pid) { const name = await mazyarFetchPlayerName(pid); if (name) { await this.#db.comment.update({ pid: pid, sport: this.#sport }, { name }); } return name; } async #deleteAllComments() { await this.#db.comment.where('sport').equals(this.#sport).delete() .then((deletedCount) => { console.log(`Deleted ${deletedCount} records.`); }) .catch((err) => { console.error("Error deleting records:", err); }); } async #deletePlayerCommentFromIndexedDb(pid) { if (pid) { await this.#db.comment.delete([this.#sport, pid]) .catch((err) => console.warn(err)); } } async #fetchScoutReportFromIndexedDb(pid) { return await this.#db.scout.get({ pid, sport: this.#sport }); } async #setScoutReportInIndexedDb(report) { if (report) { report.sport = this.#sport; report.ts = Date.now(); await this.#db.scout.put(report); } } async #setHitInIndexedDb(filterId = "", playerId = "") { if (filterId && playerId) { await this.#db.hit.put({ fid: filterId, pid: playerId }); } } async #removeHitFromIndexedDb(filterId = "", playerId = "") { if (filterId && playerId) { await this.#db.hit.delete([filterId, playerId]); } } async #removeAllHitsOfFilterFromIndexedDb(filterId = "") { if (filterId) { await this.#db.hit.where("fid").equals(filterId).delete(); } } async #getHitsFromIndexedDb(filterId = "") { if (filterId) { return this.#db.hit.filter((hit) => hit.fid === filterId).toArray(); } return []; } async #setFilterHitsInIndexedDb(filterId = "", totalHits = 0, scoutHits = -1) { if (filterId) { const item = { fid: filterId, totalHits, scoutHits, lastCheck: Date.now(), }; await this.#db.filter.put(item); } } async getFilterHitsFromIndexedDb(filterId = "") { if (filterId) { const filter = await this.#db.filter.get(filterId); if (filter) { return { totalHits: filter.totalHits, scoutHits: filter.scoutHits, lastCheck: filter.lastCheck }; } } return { totalHits: -1, scoutHits: -1, lastCheck: Date.now() }; } async #removeFilterFromIndexedDb(filterId = "") { await this.#db.filter.delete(filterId); await this.#removeAllHitsOfFilterFromIndexedDb(filterId); } async #removeAllFilterFromIndexedDb() { await this.#db.filter.clear(); await this.#db.hit.clear(); } async #fetchPlayerProfileFromIndexedDb(pid) { return await this.#db.player.get({ pid, sport: this.#sport }); } async #setPlayerProfileInIndexedDb(profile) { if (profile) { profile.sport = this.#sport; profile.ts = Date.now(); await this.#db.player.put(profile); } } async #fetchOrExtractPlayerProfile(playerId) { let profile = await this.#fetchPlayerProfileFromIndexedDb(playerId); if (!profile || (profile.days > 0 && !profile.price) || (profile.camp === undefined)) { profile = await mazyarExtractPlayerProfile(playerId); this.#setPlayerProfileInIndexedDb(profile); } return profile; } async #fetchOrExtractPlayerScoutReport(player) { const playerId = player.querySelector("h2 a")?.href?.match(/pid=(\d+)/)?.[1]; let report = await this.#fetchScoutReportFromIndexedDb(playerId); if (!report) { const skills = mazyarExtractSkillNamesFromPlayerInfo(player); report = await mazyarExtractPlayerScoutReport(playerId, skills, this.#sport); if (report) { this.#setScoutReportInIndexedDb(report); } } return report; } async #getPlayerScoutReportForSearchResult(players) { const jobs = []; for (const player of [...players.children]) { if (player.classList.contains("playerContainer") && hasPlayerScoutReport(player)) { jobs.push(this.#fetchOrExtractPlayerScoutReport(player)); } } const reports = await Promise.all(jobs); return reports.filter((report) => report != null); } #isQualifiedForTransferScoutFilter(report, lows = [], highs = []) { return highs.includes(report.H) && lows.includes(report.L); } #areTransferLowOptionsSelected() { return this.#transferOptions.L2 || this.#transferOptions.L1; } #areTransferHighOptionsSelected() { return this.#transferOptions.H4 || this.#transferOptions.H3; } #areTransferScoutOptionsSelected() { return this.#areTransferLowOptionsSelected() || this.#areTransferHighOptionsSelected(); } #getAcceptableHighsAndLows() { let lows = []; let highs = []; if (this.#areTransferLowOptionsSelected()) { if (this.#transferOptions.L2) { lows.push(2); } if (this.#transferOptions.L1) { lows.push(1); } } else { lows = [1, 2, 3]; } if (this.#areTransferHighOptionsSelected()) { if (this.#transferOptions.H4) { highs.push(4); } if (this.#transferOptions.H3) { highs.push(3); } } else { highs = [1, 2, 3, 4]; } return { lows, highs }; } #applyTransferFilters(report, lows, highs) { if (!this.#isQualifiedForTransferScoutFilter(report, lows, highs)) { const player = document.getElementById(`player_id_${report.pid}`); if (player) { const playerContainer = player.parentNode.parentNode.parentNode; if (this.#transferOptionsMustHide()) { playerContainer.classList.add("mazyar-hide"); } else { playerContainer.classList.add("mazyar-dim-60"); } } } } #clearTransferFilters() { document.querySelectorAll("#players_container .mazyar-hide")?.forEach((el) => { el.classList.remove("mazyar-hide"); }); document.querySelectorAll(".mazyar-dim-50")?.forEach((el) => el.classList.remove("mazyar-dim-50")); document.querySelectorAll(".mazyar-dim-60")?.forEach((el) => el.classList.remove("mazyar-dim-60")); } #doesTransferNeedPlayerProfile() { return this.#isDaysAtThisClubEnabledForTransferMarket() || this.#mustMarkMaxedSkills() || this.#isTransferCampEnabled(); } #addCampIconToTransferProfile(player, isSent) { if (player.campInjected) { return; } player.campInjected = true; const camp = document.createElement("tr"); const label = document.createElement("td"); label.innerText = "Camp Status:"; const status = document.createElement("td"); status.innerHTML = `<img style="height:1rem; border: solid 1px;" src="nocache-904/img/player/icon_trainingcamp_${isSent ? "dg" : "bw"}.png">` camp.appendChild(label); camp.appendChild(status); const target = player.querySelector("div.floatLeft table td:nth-child(2) > table tbody"); target?.appendChild(camp); } async #updateProfileRelatedFieldsInTransfer(player) { const playerId = mazyarExtractPlayerIdFromContainer(player); await this.#fetchOrExtractPlayerProfile(playerId).then((profile) => { if (this.#isDaysAtThisClubEnabledForTransferMarket()) { this.#squadAddDaysAtThisClubForSinglePlayer(player, profile); } if (this.#mustMarkMaxedSkills()) { mazyarColorizeMaxedSkills(player, profile?.maxed); } if (this.#isTransferCampEnabled()) { this.#addCampIconToTransferProfile(player, profile?.camp); } }); }; async #hidePlayerAccordingToHideList(player) { const playerId = mazyarExtractPlayerIdFromContainer(player); if (await this.#isPlayerInHideListInIndexDb(playerId)) { player.style.display = 'none'; } } #addHideButtonToPlayerInTransferMarket(player) { if (player.hideButtonInjected) { return; } player.hideButtonInjected = true; const playerId = mazyarExtractPlayerIdFromContainer(player); const hideIcon = mazyarCreateHideFromTransferIcon("Hide player from search result."); player.querySelector("h2.clearfix div")?.appendChild(hideIcon); hideIcon.addEventListener("click", () => { this.#addPlayerToHideListInIndexDb(playerId); player.style.display = 'none'; }); } async #addCommentIconToPlayerInTransferMarket(player) { if (player.commentIconInjected) { return; } player.commentIconInjected = true; const icon = mazyarCreateCommentIconForTransferResults("Add/See player's comment."); const playerId = mazyarExtractPlayerIdFromContainer(player); if (await this.#fetchPlayerCommentFromIndexedDb(playerId)) { icon.classList.add("mazyar-player-comment-icon-active"); } else { icon.classList.add("mazyar-player-comment-icon-inactive"); } player.querySelector("h2.clearfix div")?.appendChild(icon); icon.addEventListener("click", (event) => { const playerName = mazyarExtractPlayerNameFromContainer(player); this.#displayPlayerComment(event?.target, playerId, playerName); }); } async #processTransferSearchResults(results) { const { lows, highs } = this.#getAcceptableHighsAndLows(); const players = [...results.children].filter((player) => player.classList.contains("playerContainer")); const deadlines = await this.#fetchDeadlinePlayersFromIndexedDb(); const jobs = []; for (const player of players) { this.#addHideButtonToPlayerInTransferMarket(player); if (this.#isTransferDeadlineAlertEnabled()) { this.#addDeadlineButtonToPlayerInTransferMarket(player, deadlines); } this.#addCommentIconToPlayerInTransferMarket(player); jobs.push(this.#hidePlayerAccordingToHideList(player)); if (this.#doesTransferNeedPlayerProfile()) { jobs.push(this.#updateProfileRelatedFieldsInTransfer(player)); } if (this.#areTransferScoutOptionsSelected()) { if (hasPlayerScoutReport(player)) { jobs.push(this.#fetchOrExtractPlayerScoutReport(player).then((report) => { mazyarColorizeSkills(player, report); this.#applyTransferFilters(report, lows, highs); })); } else { const className = this.#transferOptionsMustHide() ? "mazyar-hide" : "mazyar-dim-50"; player.classList.add(className); } } } await Promise.all(jobs); } #mustUpdateDisplayForTransferSearchResults() { return this.#areTransferScoutOptionsSelected() || this.#doesTransferNeedPlayerProfile(); } async updateDisplayForTransferSearchResults(clear = false) { if (clear) { this.#clearTransferFilters(); } if (this.#mustUpdateDisplayForTransferSearchResults()) { const results = document.querySelector("#players_container > div"); if (results) { await this.#processTransferSearchResults(results); } } } #injectHideButtonToTransferMarket() { const target = document.querySelector("#searchform div.buttons-wrapper.clearfix"); if (target) { const hideButton = mazyarCreateMzStyledButton("MZY Hide", "red", "floatLeft"); hideButton.style.margin = "0"; hideButton.onclick = () => { this.#displayTransferHideMenu(); }; target.appendChild(hideButton); } } async executeTransferTasks() { this.#injectHideButtonToTransferMarket(); const callback = (mutationsList) => { if (mutationsList.find(mutation => mutation.type == "childList")) { this.updateDisplayForTransferSearchResults(); } }; setInterval(() => { const target = document.querySelector("#players_container > div"); if (target && !target.observed) { target.observed = true; this.updateDisplayForTransferSearchResults(); const observer = new MutationObserver(callback); const config = { childList: true }; observer.observe(target, config); } }, 1000); } #getSelectedScoutsOptionText() { const texts = []; const options = this.getTransferOptions(); if (options.H4) { texts.push('<span class="mazyar-scout-4">H4</span>'); } if (options.H3) { texts.push('<span class="mazyar-scout-3">H3</span>'); } if (options.L2) { texts.push('<span class="mazyar-scout-2">L2</span>'); } if (options.L1) { texts.push('<span class="mazyar-scout-1">L1</span>'); } return texts.join(", "); } #getSelectedScoutsOptionTextForFilter(scout) { const texts = []; if (scout.high.includes(4)) { texts.push('<span class="mazyar-scout-4">H4</span>'); } if (scout.high.includes(3)) { texts.push('<span class="mazyar-scout-3">H3</span>'); } if (scout.low.includes(2)) { texts.push('<span class="mazyar-scout-2">L2</span>'); } if (scout.low.includes(1)) { texts.push('<span class="mazyar-scout-1">L1</span>'); } return texts.join(", "); } #monitorCreateNoPlayerWarning() { const warning = document.createElement("div"); warning.classList.add("mazyar-monitor-player-row"); warning.innerText = "No players have been added to the MZY Deadline Monitor."; warning.style.padding = "1rem 0.5rem 0"; return warning; } #monitorAddPlayers(tbody, players) { mazyarClearPlayerRowsInMonitor(tbody); if (players.length === 0) { const warning = this.#monitorCreateNoPlayerWarning(); tbody.appendChild(warning); return; } const deadlineTimeout = this.#getTransferDeadlineTimeout(); for (const player of players) { const row = mazyarCreatePlayerRowForMonitor(player, deadlineTimeout); const removeIcon = mazyarCreateAddToDeadlineIcon("Remove player from MZY Deadline Monitor", "red"); removeIcon.style.fontSize = "11px"; row.querySelector("table.deadline-table tbody tr").appendChild(removeIcon); tbody.appendChild(row); const separators = monitorAddRowSeparator(); separators?.forEach((sep) => tbody.appendChild(sep)); removeIcon.addEventListener("click", async () => { await this.#removePlayerFromDeadlineList(player.pid); this.#deadlineUpdateIconStyle(); tbody.removeChild(row); separators.forEach((sep) => tbody.removeChild(sep)); if (!tbody.querySelector(".mazyar-monitor-player-row")) { const warning = this.#monitorCreateNoPlayerWarning(); tbody.appendChild(warning); } }); } } monitorInject() { if (this.#isTransferDeadlineAlertEnabled()) { const target = document.querySelector('div.baz.bazCenter >div > div.win_back > table'); if (target) { const monitor = mazyarCreateSectionForMonitor("MZY Monitor", "mazyar-monitor-section"); target.appendChild(monitor); target.appendChild(mazyarCreateSectionSeparatorForMonitor()); const tbody = monitor.querySelector("td > div > table > tbody"); document.body.addEventListener("deadlines-updated", () => { const players = Object.values(this.#deadlines) .filter((player) => player.source === "mzy") .sort((a, b) => a.deadline - b.deadline); this.#monitorAddPlayers(tbody, players); }); } } } // -------------------------------- Player Options ------------------------------------- async #addPlayerCommentIcon(player) { const playerId = mazyarExtractPlayerIdFromContainer(player); const playerName = mazyarExtractPlayerNameFromContainer(player); const parent = document.createElement("span"); parent.classList.add("player_icon_placeholder"); const a = document.createElement("a"); a.classList.add("player_icon"); parent.appendChild(a); const iconSpan = document.createElement("span"); iconSpan.classList.add("player_icon_wrapper"); a.appendChild(iconSpan); const commentIcon = mazyarCreateCommentIcon("MZY Comment"); commentIcon.style.fontSize = "1.2rem"; iconSpan.appendChild(commentIcon); if (location.hostname === 'www.managerzone.com') { parent.classList.add("mazyar-player-comment-icon-www"); } if (await this.#fetchPlayerCommentFromIndexedDb(playerId)) { commentIcon.classList.add("mazyar-player-comment-icon-active"); } else { commentIcon.classList.add("mazyar-player-comment-icon-inactive"); } commentIcon.addEventListener("click", (event) => { this.#displayPlayerComment(event?.target, playerId, playerName); }); const whitespace = document.createTextNode(" "); player.querySelector(".p_links")?.appendChild(whitespace); player.querySelector(".p_links")?.appendChild(parent); } async addPlayerComment() { if (this.#mustAddPlayerComment()) { const jobs = []; const players = document.querySelectorAll(".playerContainer"); for (const player of players) { jobs.push(this.#addPlayerCommentIcon(player)); } Promise.all(jobs); } } async #squadAddDaysAtThisClubForSinglePlayer(player, profile, addPrice = true) { if (player.daysInjected) { return; } player.daysInjected = true; const daysDiv = document.createElement("div"); if (profile?.days >= 0) { const text = profile?.days === 0 ? 'N/A' : `≤ ${profile?.days}`; daysDiv.innerHTML = `Days at this club: <strong>${text}</strong>`; if (addPrice && profile?.price !== null) { daysDiv.innerHTML += ` <span style="margin-left: 25px;">Transfer Fee: <strong style="color: blue;">${profile?.price}</strong><span>`; } daysDiv.classList.add("mazyar-days-at-this-club"); } else if (this.isDaysAtThisClubEnabledForOneClubPlayers()) { const text = 'Entire Career'; daysDiv.innerHTML = `Days at this club: <strong>${text}</strong>`; daysDiv.classList.add("mazyar-days-at-this-club", "mazyar-days-entire"); } player.querySelector("div.mainContent")?.appendChild(daysDiv); } async addDaysAtThisClubToAllPlayers(container) { if (this.isDaysAtThisClubEnabledForPlayerProfiles()) { const jobs = []; const players = container?.querySelectorAll("div.playerContainer"); for (const player of players) { jobs.push((async (player) => { const playerId = mazyarExtractPlayerIdFromContainer(player); await this.#fetchOrExtractPlayerProfile(playerId).then((profile) => { this.#squadAddDaysAtThisClubForSinglePlayer(player, profile); }); })(player)); } Promise.all(jobs); } } async trainingAddDaysAtThisClubToPlayerProfile(container) { if (this.#isDaysAtThisClubEnabledForTraining()) { const player = container.querySelector("div.playerContainer"); const playerId = mazyarExtractPlayerIdFromContainer(player); await this.#fetchOrExtractPlayerProfile(playerId).then((profile) => { this.#squadAddDaysAtThisClubForSinglePlayer(player, profile); }); } } // -------------------------------- Filters ------------------------------------- #fetchFilters() { this.#filters = GM_getValue("transfer_filters", { soccer: [], hockey: [] }); } #saveFilters() { GM_setValue("transfer_filters", { soccer: this.#filters.soccer.map(({ id, name, params, scout, interval, disable }) => ({ id, name, params, scout, interval, disable, })), hockey: this.#filters.hockey.map(({ id, name, params, scout, interval, disable }) => ({ id, name, params, scout, interval, disable, })), }); } #getCurrentFilters() { return this.#filters[this.#sport]; } deleteFilter(id = "") { const filterIndex = this.#getCurrentFilters().findIndex((f) => f.id === id); if (filterIndex > -1) { this.#removeFilterFromIndexedDb(id); this.#getCurrentFilters().splice(filterIndex, 1); this.#checkAllFilters(true); this.#saveFilters(); } } deleteAllFilters() { this.#removeAllFilterFromIndexedDb(); this.#getCurrentFilters().length = 0; this.#checkAllFilters(true); this.#saveFilters(); } getFilterParams(name = "") { return this.#getCurrentFilters().find((f) => f.name === name)?.params; } async #updateFilterDetails(name, params, scout, interval) { const filters = this.#getCurrentFilters(); let filter = filters.find((f) => f.name === name); if (filter) { filter.params = params; filter.scout = scout; filter.interval = interval; } else { // create a new filter if name does not exist filter = { name, params, scout, interval, disable: false, id: mazyarGenerateUuidV4(), }; filters.push(filter); } this.#saveFilters(); const { totalHits, scoutHits } = await this.#getFilterTotalHits(filter); if (totalHits >= 0) { await this.#setFilterHitsInIndexedDb(filter.id, totalHits, scoutHits); this.#checkAllFilters(true); } } #itsTimeToCheckFilter(filter, lastCheck) { const passed = Date.now() - lastCheck; if (filter.interval === MAZYAR_TRANSFER_INTERVALS.never.value) { return false; } else if (filter.interval === MAZYAR_TRANSFER_INTERVALS.onceDay.value) { return passed > 24 * 60 * 60 * 1000; } else if (filter.interval === MAZYAR_TRANSFER_INTERVALS.onceHour.value) { return passed > 1 * 60 * 60 * 1000; } else if (filter.interval === MAZYAR_TRANSFER_INTERVALS.onceMinute.value) { return passed > 1 * 60 * 1000; } // 'always' or any invalid value means always return true; } #getAcceptableHighsAndLowsForFilter(scout) { const lows = scout.low.length > 0 ? scout.low : [1, 2, 3]; const highs = scout.high.length > 0 ? scout.high : [1, 2, 3, 4]; return { lows, highs }; } async #getFilterHitsByOffset(filter, offset = 0) { let totalHits = -1; let scoutHits = -1; const url = `https://${location.hostname}/ajax.php?p=transfer&sub=transfer-search&sport=${this.#sport}${filter.params}&o=${offset}`; const data = await mazyarFetchJson(url); if (data) { totalHits = Number(data?.totalHits); const searchResults = document.createElement("div"); searchResults.innerHTML = data.players; if (filter.scout) { const playersReport = await this.#getPlayerScoutReportForSearchResult(searchResults.firstChild); const { lows, highs } = this.#getAcceptableHighsAndLowsForFilter(filter.scout); const scouted = playersReport.filter((report) => this.#isQualifiedForTransferScoutFilter(report, lows, highs)); scoutHits = scouted.length; // save scout hits in indexed db const jobs = []; for (const player of scouted) { jobs.push(this.#setHitInIndexedDb(filter.id, player.pid)); } await Promise.all(jobs); } } return { totalHits, scoutHits }; } async #getFilterTotalHits(filter) { // get first page of the results await this.#removeAllHitsOfFilterFromIndexedDb(filter.id); let { totalHits, scoutHits } = await this.#getFilterHitsByOffset(filter); if (totalHits >= 0) { if (filter.scout) { const pages = Math.min(Math.ceil(totalHits / 20), 26); const jobs = []; // when filter uses scout report, get next pages too for (let i = 1; i < pages; i++) { jobs.push(this.#getFilterHitsByOffset(filter, 20 * i)); } const results = await Promise.all(jobs); for (const result of results) { scoutHits += result.scoutHits; } } } return { totalHits, scoutHits }; } async #refreshFilterHits(id = "") { const filter = this.#getCurrentFilters().find((filter) => filter.id === id); if (filter) { const { totalHits, scoutHits } = await this.#getFilterTotalHits(filter); if (totalHits >= 0) { this.#setFilterHitsInIndexedDb(filter.id, totalHits, scoutHits); this.#checkAllFilters(false); return { totalHits, scoutHits }; } } return { totalHits: -1, scoutHits: -1 }; } // -------------------------------- Display ------------------------------------- #createModal() { const overlay = document.createElement("div"); overlay.id = "mazyar-modal-overlay"; overlay.classList.add("mazyar-flexbox-column", "mazyar-hide"); const modal = document.createElement("div"); modal.id = "mazyar-modal"; modal.classList.add("mazyar-flexbox-column", "mazyar-scrollable-vertical"); overlay.appendChild(modal); window.__mzcMega.mountDonorContent("mazyar", overlay, "MZY"); } #hideModal() { document.getElementById("mazyar-modal-overlay").classList.add("mazyar-hide"); document.getElementById("mazyar-modal").replaceChildren(); } #showModal(elements = []) { document.getElementById("mazyar-modal").replaceChildren(...elements); document.getElementById("mazyar-modal-overlay").classList.remove("mazyar-hide"); } #createModalBody() { const body = document.createElement("div"); body.classList.add("mazyar-flexbox-column", "mazyar-modal-body"); return body; } #createModalFooter() { const footer = document.createElement("div"); footer.classList.add("mazyar-flexbox-row", "mazyar-modal-footer"); return footer; } #addToolbar() { const { toolbar, menu, transfer, note, comments, live } = mazyarCreateToolbar(); menu.addEventListener("click", () => { this.#displaySettingsMenu(); }); transfer.addEventListener("click", () => { this.#displayTransferFilters(); }); comments.addEventListener("click", () => { this.#displayPlayersCommentList(); }); note.addEventListener("click", () => { if (this.#notebook.style.hide) { this.#showNotebook(); } else { this.#hideNotebook(); } }); live.style.color = this.mustDisplayInProgressResults() ? "greenyellow" : "unset"; live.addEventListener("click", () => { if (this.mustDisplayInProgressResults()) { this.#disableDisplayingInProgressResults(); live.style.color = "unset"; } else { this.#enableDisplayingInProgressResults(); live.style.color = "greenyellow"; } if (this.#couldInjectInProgressResults()) { location.reload(); } }); document.addEventListener("focus", () => { this.#updateInProgressIconDisplay(live); }); window.__mzcMega.mountDonorToolbar("mazyar", toolbar, "MZY"); // updates are owned by mzcopilot } #couldInjectInProgressResults() { const uri = document.baseURI; return (uri.search("/?p=match") > -1 && (uri.search("&sub=result") < 0)) || (uri.search("/?p=cup&") > -1) || (uri.search("/?p=private_cup&") > -1) || (uri.search("/?p=friendlyseries") > -1) || (uri.search("/?p=league") > -1); } #updateInProgressIconDisplay(icon) { const last = this.mustDisplayInProgressResults(); this.#settings.miscellaneous.in_progress_results = GM_getValue("display_in_progress_results", true); const current = this.mustDisplayInProgressResults(); if (last !== current) { icon.style.color = current ? "greenyellow" : "unset"; if (this.#couldInjectInProgressResults()) { location.reload(); } } } #displayLoading(title) { const header = mazyarCreateMzStyledModalHeader(title); const body = this.#createModalBody(); body.style.margin = "1rem"; const loading = mazyarCreateLoadingIcon2(); loading.style.fontSize = "x-large"; body.appendChild(loading); this.#showModal([header, body]); } #displayCleanInstallMenu() { const header = mazyarCreateMzStyledModalHeader("MZY Settings - Clean", () => { this.#hideModal(); this.#displaySettingsMenu(); }); const body = this.#createModalBody(); const notice = document.createElement("div"); notice.innerHTML = "All Settings, Filters, Scout Reports and ... will be deleted.<br>Are you sure?"; notice.style.padding = "1rem"; body.appendChild(notice); const footer = this.#createModalFooter(); const cancel = mazyarCreateMzStyledButton("Cancel", "red"); const clean = mazyarCreateMzStyledButton("Clean", "blue"); footer.appendChild(cancel); footer.appendChild(clean); clean.addEventListener("click", async () => { await this.#cleanInstall(); this.#hideModal(); this.#displaySettingsMenu(); }); cancel.addEventListener("click", () => { this.#hideModal(); this.#displaySettingsMenu(); }); this.#showModal([header, body, footer]); } // --------------------------- Notebook ------------------------------------ #fetchNotebookStyle() { const defaultStyle = { hide: true, top: 0, left: 0, width: 200, height: 250, }; this.#notebook.style = GM_getValue("notebook_style", defaultStyle); // reject invalid data if (this.#notebook.style.top < 0 || this.#notebook.style.left < 0 || this.#notebook.style.left > window.innerWidth || this.#notebook.style.top > window.innerHeight) { this.#notebook.style.top = 0; this.#notebook.style.left = 0; this.#saveNotebookStyle(); } } #saveNotebookStyle() { GM_setValue("notebook_style", this.#notebook.style); } #fetchNotebookText() { this.#notebook.text = GM_getValue("notebook_text", ""); } #saveNotebookText() { GM_setValue("notebook_text", this.#notebook.text); } #updateNotebookDisplay(overlay, modal, text) { this.#fetchNotebookStyle(); this.#fetchNotebookText(); if (this.#notebook.style.hide) { overlay?.classList.add("mazyar-hide"); } else { overlay?.classList.remove("mazyar-hide"); } text.value = this.#notebook.text; modal.style.width = this.#notebook.style.width + "px"; modal.style.height = this.#notebook.style.height + "px"; modal.style.top = this.#notebook.style.top + "px"; modal.style.left = this.#notebook.style.left + "px"; } #updateNotebookLocation(modal) { const { top, left, width, height } = modal.getBoundingClientRect(); this.#notebook.style.top = top; this.#notebook.style.left = left; this.#notebook.style.width = width; this.#notebook.style.height = height; } #hideNotebook() { document.getElementById("mazyar-notebook-overlay")?.classList.add("mazyar-hide"); this.#notebook.style.hide = true; this.#saveNotebookStyle(); } #showNotebook() { document.getElementById("mazyar-notebook-overlay")?.classList.remove("mazyar-hide"); this.#notebook.style.hide = false; this.#saveNotebookStyle(); } #showNotebookEditButtons(save, discard, warning) { save.style.display = "unset"; discard.style.display = "unset"; warning.style.display = "unset"; } #hideNotebookEditButtons(save, discard, warning) { save.style.display = "none"; discard.style.display = "none"; warning.style.display = "none"; } #createNotebook() { const overlay = document.createElement("div"); overlay.id = "mazyar-notebook-overlay"; overlay.classList.add("mazyar-flexbox-column", "mazyar-scrollable-vertical"); const modal = document.createElement("div"); modal.id = "mazyar-notebook"; modal.classList.add("mazyar-flexbox-column", "mazyar-resizable", "mazyar-scrollable-vertical"); overlay.appendChild(modal); const header = mazyarCreateMzStyledModalHeader("MZY Notebook", () => { this.#hideNotebook(); }); const text = document.createElement("textarea"); text.classList.add("mazyar-notebook-textarea"); const warning = document.createElement("div"); warning.innerText = "You have unsaved changes!"; warning.style.color = "red"; warning.style.marginTop = "5px"; const footer = document.createElement("div"); footer.classList.add("mazyar-flexbox-row"); const hide = mazyarCreateMzStyledButton("Hide", "blue"); const save = mazyarCreateMzStyledButton("Save", "green"); const discard = mazyarCreateMzStyledButton("Discard", "red"); footer.appendChild(hide); footer.appendChild(discard); footer.appendChild(save); modal.appendChild(header); modal.appendChild(text); modal.appendChild(warning); modal.appendChild(footer); this.#hideNotebookEditButtons(save, discard, warning); this.#updateNotebookDisplay(overlay, modal, text); document.addEventListener("focus", () => { this.#updateNotebookDisplay(overlay, modal, text); this.#hideNotebookEditButtons(save, discard, warning); }); mazyarMakeElementDraggable(modal, header, () => { this.#updateNotebookLocation(modal); this.#saveNotebookStyle(); }); modal.addEventListener("mouseup", () => { this.#updateNotebookLocation(modal); this.#saveNotebookStyle(); }) text.addEventListener("input", () => { if (text.value !== this.#notebook.text) { this.#showNotebookEditButtons(save, discard, warning); } else { this.#hideNotebookEditButtons(save, discard, warning); } }) discard.addEventListener("click", () => { text.value = this.#notebook.text; this.#hideNotebookEditButtons(save, discard, warning); }); hide.addEventListener("click", () => { this.#hideNotebook(); }); save.addEventListener("click", () => { this.#notebook.text = text.value; this.#saveNotebookText(); this.#hideNotebookEditButtons(save, discard, warning); }); document.body?.appendChild(overlay); } // ---------------------------------------------------------------------------------- #createDeadlineOptions(level1Style, level2Style) { const div = document.createElement("div"); div.classList.add("mazyar-flexbox-column"); const enabled = mazyarCreateMenuCheckBox("Enable deadline alert", this.#settings.transfer.deadline.enabled, level1Style); const playBell = mazyarCreateMenuCheckBox("Sound Notification", this.#settings.transfer.deadline.play_bell, level2Style); const timeout = mazyarCreateSubMenuTextInput("Timeout", "30", this.#settings.transfer.deadline.timeout); const unit = document.createTextNode("minute(s)"); timeout.appendChild(unit); timeout.style.display = this.#settings.transfer.deadline.enabled ? "unset" : "none"; playBell.style.display = this.#settings.transfer.deadline.enabled ? "unset" : "none"; div.appendChild(enabled); // child node 0 div.appendChild(timeout); // child node 1 div.appendChild(playBell); // child node 2 enabled.addEventListener("input", () => { timeout.style.display = enabled.querySelector("input[type='checkbox']").checked ? "unset" : "none"; playBell.style.display = enabled.querySelector("input[type='checkbox']").checked ? "unset" : "none"; }); timeout.addEventListener("input", () => { const minutes = timeout.querySelector("input[type='text']"); if (minutes?.value?.match(/^\d+$/)) { const value = Number(minutes?.value) if (value < 1) { minutes.value = "1"; } else if (value > 360) { minutes.value = "360"; } } else { minutes.value = ""; } }); return div; } #displayTransferSettingsMenu() { const level1Style = { margin: "0.3rem 0.7rem" }; const level2Style = { margin: "0.3rem 1.2rem" }; const header = mazyarCreateMzStyledModalHeader(`MZY Transfer Settings`, () => { this.#hideModal(); this.#displaySettingsMenu(); }); const body = this.#createModalBody(); const filters = mazyarCreateMenuCheckBox("Enable transfer filters", this.#settings.transfer.enable_filters, level1Style); const maxed = mazyarCreateMenuCheckBox("Mark maxed skills", this.#settings.transfer.display_maxed, level1Style); const camp = mazyarCreateMenuCheckBox("Check if player is sent to camp", this.#settings.transfer.display_camp, level1Style); const deadline = this.#createDeadlineOptions(level1Style, level2Style); body.appendChild(filters); body.appendChild(maxed); body.appendChild(camp); body.appendChild(deadline); const footer = this.#createModalFooter(); const cancel = mazyarCreateMzStyledButton("Cancel", "red"); const save = mazyarCreateMzStyledButton("Save", "green"); footer.appendChild(cancel); footer.appendChild(save); cancel.addEventListener("click", () => { this.#hideModal(); this.#displaySettingsMenu(); }); save.onclick = () => { const deadlineTimeout = Number(deadline.childNodes[1].querySelector("input[type=text]")?.value); this.#updateTransferSettings({ enable_filters: filters.querySelector("input[type=checkbox]").checked, display_maxed: maxed.querySelector("input[type=checkbox]").checked, display_camp: camp.querySelector("input[type=checkbox]").checked, deadline: { enabled: deadline.childNodes[0].querySelector("input[type=checkbox]")?.checked, play_bell: deadline.childNodes[2].querySelector("input[type=checkbox]")?.checked, timeout: deadlineTimeout > 0 && deadlineTimeout <= 360 ? deadlineTimeout : 30, }, }); this.#hideModal(); this.#displaySettingsMenu(); }; this.#showModal([header, body, footer]); } #displayDaysSettingsMenu() { const level1Style = { margin: "0.3rem 0.7rem" }; const header = mazyarCreateMzStyledModalHeader(`MZY Days Settings`, () => { this.#hideModal(); this.#displaySettingsMenu(); }); const body = this.#createModalBody(); const daysInProfiles = mazyarCreateMenuCheckBox("Display in player profiles", this.#settings.days.display_in_profiles, level1Style); const daysInTransfer = mazyarCreateMenuCheckBox("Display in transfer market", this.#settings.days.display_in_transfer, level1Style); const daysInTraining = mazyarCreateMenuCheckBox("Display in training report", this.#settings.days.display_in_training, level1Style); const daysForOneClubs = mazyarCreateMenuCheckBox("Display for One-Club players", this.#settings.days.display_for_one_clubs, level1Style); body.appendChild(daysInProfiles); body.appendChild(daysInTransfer); body.appendChild(daysInTraining); body.appendChild(daysForOneClubs); const footer = this.#createModalFooter(); const cancel = mazyarCreateMzStyledButton("Cancel", "red"); const save = mazyarCreateMzStyledButton("Save", "green"); footer.appendChild(cancel); footer.appendChild(save); cancel.addEventListener("click", () => { this.#hideModal(); this.#displaySettingsMenu(); }); save.onclick = () => { this.#updateDaysSettings({ display_in_profiles: daysInProfiles.querySelector("input[type=checkbox]").checked, display_in_transfer: daysInTransfer.querySelector("input[type=checkbox]").checked, display_in_training: daysInTraining.querySelector("input[type=checkbox]").checked, display_for_one_clubs: daysForOneClubs.querySelector("input[type=checkbox]").checked, }); this.#hideModal(); this.#displaySettingsMenu(); }; this.#showModal([header, body, footer]); } #displayMiscellaneousSettingsMenu() { const level1Style = { margin: "0.3rem 0.7rem" }; const header = mazyarCreateMzStyledModalHeader(`MZY Miscellaneous Settings`, () => { this.#hideModal(); this.#displaySettingsMenu(); }); const body = this.#createModalBody(); const playerComment = mazyarCreateMenuCheckBox("Enable player comment", this.#settings.miscellaneous.player_comment, level1Style); const inProgress = mazyarCreateMenuCheckBox("Display in progress results", this.#settings.miscellaneous.in_progress_results, level1Style); const tableInjection = mazyarCreateMenuCheckBox("Display teams' top players in tables", this.#settings.miscellaneous.top_players_in_tables, level1Style); const mzPredictor = mazyarCreateMenuCheckBox("Help with World Cup Predictor", this.#settings.miscellaneous.mz_predictor, level1Style); const fixtureFullName = mazyarCreateMenuCheckBox("Display team's full name in fixture", this.#settings.miscellaneous.fixture_full_name, level1Style); const tableTransferList = mazyarCreateMenuCheckBox("Add transfer list in tables", this.#settings.miscellaneous.table_transfer_list, level1Style); const tableTransferHistory = mazyarCreateMenuCheckBox("Add transfer history in tables", this.#settings.miscellaneous.table_transfer_history, level1Style); const coachSalaries = mazyarCreateMenuCheckBox("Display salaries in search results", this.#settings.miscellaneous.coach_salary, level1Style); mzPredictor.style.display = 'none'; body.appendChild(playerComment); body.appendChild(inProgress); body.appendChild(tableInjection); body.appendChild(fixtureFullName); body.appendChild(coachSalaries); body.appendChild(mzPredictor); body.appendChild(tableTransferList); body.appendChild(tableTransferHistory); const footer = this.#createModalFooter(); const cancel = mazyarCreateMzStyledButton("Cancel", "red"); const save = mazyarCreateMzStyledButton("Save", "green"); footer.appendChild(cancel); footer.appendChild(save); cancel.addEventListener("click", () => { this.#hideModal(); this.#displaySettingsMenu(); }); save.onclick = () => { this.#updateMiscellaneousSettings({ in_progress_results: inProgress.querySelector("input[type=checkbox]").checked, top_players_in_tables: tableInjection.querySelector("input[type=checkbox]").checked, mz_predictor: mzPredictor.querySelector("input[type=checkbox]").checked, player_comment: playerComment.querySelector("input[type=checkbox]").checked, coach_salary: coachSalaries.querySelector("input[type=checkbox]").checked, fixture_full_name: fixtureFullName.querySelector("input[type=checkbox]").checked, table_transfer_list: tableTransferList.querySelector("input[type=checkbox]").checked, table_transfer_history: tableTransferHistory.querySelector("input[type=checkbox]").checked, }); this.#hideModal(); this.#displaySettingsMenu(); }; this.#showModal([header, body, footer]); } #displaySettingsMenu() { const header = mazyarCreateMzStyledModalHeader(`MZY Settings (v${CURRENT_VERSION})`, () => { this.#hideModal(); }); const cleanTitle = `<i class="fa fa-exclamation-triangle" style="font-size: 1rem;"></i> Clean Install`; const body = this.#createModalBody(); const days = mazyarCreateSettingsSectionButton("Days Settings"); const transfer = mazyarCreateSettingsSectionButton("Transfer Settings"); const miscellaneous = mazyarCreateSettingsSectionButton("Miscellaneous Settings"); const clean = mazyarCreateSettingsSectionButton(cleanTitle, { backgroundColor: "orange" }); body.appendChild(days); body.appendChild(transfer); body.appendChild(miscellaneous); body.appendChild(clean); transfer.addEventListener("click", () => { this.#hideModal(); this.#displayTransferSettingsMenu(); }); days.addEventListener("click", () => { this.#hideModal(); this.#displayDaysSettingsMenu(); }); miscellaneous.addEventListener("click", () => { this.#hideModal(); this.#displayMiscellaneousSettingsMenu(); }); clean.addEventListener("click", () => { this.#hideModal(); this.#displayCleanInstallMenu(); }); if (this.#updateInfo.version) { const url = GM_info.script.downloadURL ?? "https://update.greasyfork.org/scripts/476290/Mazyar.user.js"; const tip = mazyarCreateUpdateTip(this.#updateInfo.version, url); body.appendChild(tip); } this.#showModal([header, body]); } #getSelectedHighLows(useScout) { const high = []; const low = []; if (useScout.querySelector("input[type=checkbox]").checked) { const options = this.getTransferOptions(); if (options.H4) { high.push(4); } if (options.H3) { high.push(3); } if (options.L2) { low.push(2); } if (options.L1) { low.push(1); } } return { low, high }; } displayFilterSaveMenu(params) { const filters = this.#getCurrentFilters(); const scoutText = this.#getSelectedScoutsOptionText(); const header = mazyarCreateMzStyledModalHeader("MZY Transfer Filter", () => { this.#hideModal(); }); const body = this.#createModalBody(); const datalist = mazyarCreateSuggestionList(filters.map((f) => f.name)); const filterName = mazyarCreateMenuTextInput("Filter Name", "U21 K-10 ST-10", datalist.id); const useScout = mazyarCreateMenuCheckBox(`Use scout reports too (${scoutText})`); const checkInterval = mazyarCreateDropDownMenu("Check Interval", MAZYAR_TRANSFER_INTERVALS, MAZYAR_TRANSFER_INTERVALS.onceHour.value); const validation = document.createElement("div"); body.appendChild(filterName); body.appendChild(checkInterval); if (scoutText) { body.appendChild(useScout); } body.appendChild(datalist); body.appendChild(validation); const footer = this.#createModalFooter(); const save = mazyarCreateMzStyledButton("Save", "green"); footer.appendChild(save); validation.innerText = "Error: Name is empty."; validation.style.color = "red"; validation.style.display = "none"; validation.style.alignSelf = "flex-start"; validation.style.margin = "auto 0.5rem"; filterName.oninput = () => { const name = filterName.querySelector("input[type='text']").value; if (name) { validation.style.display = "none"; save.classList.remove(mazyarGetMzButtonColorClass("grey")); } else { validation.style.display = "unset"; save.classList.add(mazyarGetMzButtonColorClass("grey")); } }; save.addEventListener("click", () => { // save then close const name = filterName.querySelector("input[type=text]").value; if (name) { const { low, high } = this.#getSelectedHighLows(useScout); const scout = high.length === 0 && low.length === 0 ? null : { high, low }; const interval = checkInterval.querySelector("select").value; this.#updateFilterDetails(name, params, scout, interval); this.#hideModal(); } else { validation.style.display = "unset"; save.classList.add(mazyarGetMzButtonColorClass("grey")); } }); this.#showModal([header, body, footer]); } #setFilterHitsInToolbar(total = -1) { // pass negative to create loading animation. const hits = document.getElementById("mazyar-transfer-filter-hits"); if (hits) { if (total < 0) { hits.replaceChildren(mazyarCreateLoadingIcon()); } else { hits.innerText = total > 100 ? "+100" : total.toString(); hits.style.color = total > 0 ? "cyan" : "white"; } } } async #checkAllFilters(forced = false) { this.#setFilterHitsInToolbar(-1); const filters = this.#getCurrentFilters(); let total = 0; for (const filter of filters.filter((f) => !f.disable)) { let { totalHits: hits, lastCheck } = await this.getFilterHitsFromIndexedDb(filter.id); const needRefresh = this.#itsTimeToCheckFilter(filter, lastCheck); if (!mazyarIsFilterHitsValid(hits) || forced || needRefresh) { const { totalHits, scoutHits } = await this.#getFilterTotalHits(filter); if (totalHits >= 0) { hits = filter.scout ? scoutHits : totalHits; await this.#setFilterHitsInIndexedDb(filter.id, totalHits, scoutHits); } } total += hits; } this.#setFilterHitsInToolbar(total); return total; } async setInitialFiltersHitInToolbar() { await this.#checkAllFilters(false); } async #displayTransferHideMenu() { const header = mazyarCreateMzStyledModalHeader("MZY Transfer Hide List", () => { this.#hideModal(); }); await this.#countPlayersOfHideListInIndexDb(); const body = this.#createModalBody(); const description = document.createElement("div"); const dayClearDiv = document.createElement("div"); const daysInput = mazyarCreateMenuTextInput("Days", "0", ""); const clear = mazyarCreateMzStyledButton("Remove", "red", "floatRight"); const validation = document.createElement("div"); const result = document.createElement("div"); dayClearDiv.appendChild(daysInput); dayClearDiv.appendChild(clear); body.appendChild(description); body.appendChild(dayClearDiv); body.appendChild(validation); body.appendChild(result); body.style.maxWidth = "320px"; dayClearDiv.classList.add("mazyar-flexbox-row"); daysInput.querySelector("input[type='text']").style.width = "2rem"; description.innerHTML = `<p>There are <strong style="color:red;">${await this.#countPlayersOfHideListInIndexDb()}</strong> player(s) hidden from transfer market.</p> <p>You can remove players from hide list.</p> <p>Enter the number of days to remove players hidden before this period.<br>Enter 0 to remove all.</p>`; description.style.paddingLeft = "0.7rem"; validation.innerText = "Error: Invalid value for days."; validation.style.color = "red"; validation.style.display = "none"; validation.style.margin = "auto 0.5rem"; result.style.display = "none"; result.style.margin = "auto 0.5rem"; result.innerHTML = `Removed <strong style="color:blue;">0</strong> player(s) from hide list.</p>` daysInput.addEventListener("input", () => { result.style.display = "none"; const days = daysInput.querySelector("input[type='text']").value; if (days.match(/^\d+$/)) { validation.style.display = "none"; clear.classList.remove(mazyarGetMzButtonColorClass("grey")); } else { validation.style.display = "unset"; clear.classList.add(mazyarGetMzButtonColorClass("grey")); } }); clear.addEventListener("click", async () => { const days = daysInput.querySelector("input[type='text']").value if (days.match(/\d+/)) { const deleteCount = await this.#deletePlayersFromHideListInIndexDb(days); result.querySelector("strong").innerText = deleteCount; result.style.display = "unset"; description.querySelector("strong").innerText = await this.#countPlayersOfHideListInIndexDb(); } else { validation.style.display = "unset"; clear.classList.add(mazyarGetMzButtonColorClass("grey")); } }); this.#showModal([header, body]); } #filtersViewCreateTableBody(filters = []) { const tbody = document.createElement("tbody"); for (const filter of filters) { const tr = document.createElement("tr"); const filterName = filter.name.length > 32 ? filter.name.substring(0, 29) + "..." : filter.name; const name = document.createElement("td"); name.innerHTML = `<a target="_blank" href="https://${location.hostname}/?p=transfer&mzy_filter_name=${filter.name}">${filterName}</a>`; name.title = filter.name.length > 32 ? `Filter's full name: ${filter.name}` : "Filter's name"; const total = document.createElement("td"); const scout = document.createElement("td"); total.style.textAlign = "center"; scout.style.textAlign = "center"; if (filter.disable) { total.innerText = "-"; scout.innerText = "-"; } else { this.getFilterHitsFromIndexedDb(filter.id).then(({ totalHits, scoutHits }) => { total.innerText = mazyarIsFilterHitsValid(totalHits) ? totalHits.toString() : "n/a"; if (filter.scout) { scout.innerHTML = `<a style="cursor: pointer;">${mazyarIsFilterHitsValid(scoutHits) ? scoutHits.toString() : "n/a"}</a>`; } else { scout.innerHTML = "X"; } }); } let scoutClickable = !filter.disable; if (filter.scout) { scout.onclick = () => { if (scoutClickable) { const info = { name: filter.name, scout: filter.scout, count: scout.innerText }; this.displayFilterResults(filter.id, info); } }; } const del = mazyarCreateTrashIcon("Delete the filter permanently.", { fontSize: "1rem", margin: "0px 3px 0px 0px" }); del.onclick = () => { this.deleteFilter(filter.id); tbody.removeChild(tr); if (tbody.children.length === 0) { tbody.parentNode.dispatchEvent(new CustomEvent("destroy")); } }; const refresh = mazyarCreateRefreshIcon("Refresh filter hits."); refresh.classList.toggle("mazyar-disabled-icon", filter.disable); refresh.style.fontSize = "1.1rem"; refresh.onclick = async () => { if (!filter.disable) { scoutClickable = false; mazyarStartSpinning(refresh); total.replaceChildren(mazyarCreateLoadingIcon()); scout.replaceChildren(mazyarCreateLoadingIcon()); const { totalHits, scoutHits } = await this.#refreshFilterHits(filter.id); total.innerText = mazyarIsFilterHitsValid(totalHits) ? totalHits.toString() : "n/a"; if (filter.scout) { scout.innerHTML = `<a style="cursor: pointer;">${mazyarIsFilterHitsValid(scoutHits) ? scoutHits.toString() : "n/a"}</a>`; } else { scout.innerHTML = "X"; } mazyarStopSpinning(refresh); scoutClickable = true; } }; const disable = mazyarCreateToggleIcon("Disable/Enable the filter", !filter.disable); disable.style.fontSize = "1.2rem"; disable.style.margin = "0px 3px"; disable.addEventListener('click', () => { filter.disable = !filter.disable; disable.classList.toggle('fa-toggle-on', !filter.disable); disable.classList.toggle('fa-toggle-off', filter.disable); this.#saveFilters(); refresh.classList.toggle("mazyar-disabled-icon", filter.disable); if (filter.disable) { total.innerText = "-"; scout.innerHTML = "-"; scoutClickable = false; } }); const tools = document.createElement("td"); tools.appendChild(disable); tools.appendChild(del); tools.appendChild(refresh); tr.appendChild(tools); tr.appendChild(name); tr.appendChild(total); tr.appendChild(scout); tbody.appendChild(tr); } return tbody; } #filtersViewCreateTable(filters, destroyCallback) { const div = document.createElement("div"); div.classList.add("mazyar-modal-table-container"); const table = document.createElement("table"); const thead = mazyarCreateTableHeaderForFiltersView(); const tbody = this.#filtersViewCreateTableBody(filters); table.classList.add("mazyar-table", "tablesorter", "hitlist", "marker"); table.style.margin = "0.5rem"; table.appendChild(thead); table.appendChild(tbody); table.addEventListener("destroy", destroyCallback); div.appendChild(table); return div; } async #displayTransferFilters() { const header = mazyarCreateMzStyledModalHeader("MZY Transfer Filters", () => { this.#hideModal(); this.#checkAllFilters(true); }); const body = this.#createModalBody(); // filters table is fully shown and scrollable only when it is structured like body > div > div > table const filtersView = document.createElement("div"); const noFilterView = document.createElement("span"); body.appendChild(filtersView); body.appendChild(noFilterView); filtersView.classList.add("mazyar-flexbox-column"); noFilterView.innerText = "There is no filter to display"; noFilterView.style.display = "none"; noFilterView.style.margin = "1rem"; const filters = this.#getCurrentFilters(); if (filters.length > 0) { const deleteAll = mazyarCreateDeleteAllFiltersButton("Delete all filters", () => { this.deleteAllFilters(); filtersView.style.display = "none"; noFilterView.style.display = "unset"; }); const table = this.#filtersViewCreateTable(filters, () => { // remove 'delete all' button if no filter is left filtersView.style.display = "none"; noFilterView.style.display = "unset"; }); filtersView.appendChild(deleteAll); filtersView.appendChild(table); } else { noFilterView.style.display = "unset"; } this.#showModal([header, body]); } async displaySquadSummary(url) { this.#displayLoading("MZY Squad Summary"); const header = mazyarCreateMzStyledModalHeader("MZY Squad Summary", () => { this.#hideModal(); }); const body = this.#createModalBody(); const doc = await mazyarFetchHtml(url); if (doc) { const sport = mazyarExtractSportType(doc); const currency = mazyarExtractClubCurrency(doc); const players = mazyarExtractClubPlayersDetails(doc, currency); const summary = mazyarExtractSquadSummaryDetails(players, sport); const topPlayers = squadCreateTopPlayersTable(summary, currency, sport); topPlayers.style.margin = "2px 5px"; topPlayers.style.padding = "0"; body.appendChild(topPlayers); } else { body.innerText = "Fetching squad summary failed." } this.#showModal([header, body]); } async #displayTransferDeadlines() { const header = mazyarCreateMzStyledModalHeader("MZY Transfer Deadlines", () => { this.#hideModal(); }); const body = this.#createModalBody(); // table is fully shown and scrollable only when it is structured like body > div > div > table const divMain = document.createElement("div"); divMain.classList.add("mazyar-flexbox-column"); const divChild = document.createElement("div"); // 'overflow: auto' makes the table scrollable and fully shown divChild.classList.add("mazyar-modal-table-container"); divMain.appendChild(divChild); body.appendChild(divMain); const table = document.createElement("table"); table.classList.add("mazyar-table", "tablesorter", "hitlist", "marker"); table.style.margin = "0.5rem"; divChild.appendChild(table); const thead = document.createElement("thead"); thead.innerHTML = ` <tr> <th style="text-decoration: none; width: 15px;"></th> <th style="text-decoration: none; text-align: left; padding-left: 5px;"><strong>Player</strong></th> <th style="text-decoration: none;"><strong>Deadline</strong></th> </tr>`; table.appendChild(thead); const sortedBids = Object.values(this.#deadlines)?.sort((a, b) => a.deadline - b.deadline); const tbody = document.createElement("tbody"); table.appendChild(tbody); const deadlineTimeout = this.#getTransferDeadlineTimeout(); for (const bid of sortedBids) { const row = document.createElement("tr"); const deleteButton = document.createElement("td"); deleteButton.addEventListener("click", async () => { await this.#removePlayerFromDeadlineList(bid.pid); row.remove(); if (tbody.childElementCount === 0) { this.#deadlineUpdateIconStyle(); this.#hideModal(); } }); const trashIcon = mazyarCreateTrashIcon("Remove from deadline list", { fontSize: "0.8rem" }); deleteButton.appendChild(trashIcon); const name = document.createElement("td"); name.innerHTML = `<a href="/?p=transfer&sub=players&u=${bid.pid}" target="_blank">${bid.name}</a>`; name.style.paddingRight = "15px"; const deadline = document.createElement("td"); deadline.innerHTML = `<strong>${bid.deadline}</strong> minutes`; deadline.style.paddingRight = "15px"; if (bid.deadline <= deadlineTimeout) { deadline.style.color = "red"; } row.appendChild(deleteButton); row.appendChild(name); row.appendChild(deadline); tbody.appendChild(row); } this.#showModal([header, body]); } #addDeadlineIndicator() { const deadline = document.createElement("div"); const transferIcon = mazyarCreateLegalIcon(); transferIcon.style.fontSize = "2rem"; deadline.id = "mazyar-deadline-overlay"; deadline.classList.add("mazyar-flexbox-column", "mazyar-hide"); deadline.style.borderRadius = "50%"; deadline.appendChild(transferIcon); document.body?.appendChild(deadline); deadline.addEventListener("click", () => { this.#displayTransferDeadlines(); }); } async #monitorFetchAndProcessYourBidsPlayers() { const url = `https://${location.hostname}/ajax.php?p=transfer&sub=your-bids&sport=${this.#sport}`; const response = await mazyarFetchJson(url); if (response) { const yourBids = document.createElement("div"); yourBids.innerHTML = response.content; const bids = yourBids.querySelectorAll(`table[cellpadding="0"][border="0"] table img[src*="/flags/"]`); const rows = [...bids].map((element) => element.parentNode.parentNode.parentNode.parentNode.parentElement.parentNode); return rows.map((row) => { const sections = [...row.childNodes].filter((el) => el.tagName === "TD"); // sections are: [0: player name], [1:deadline], [2:latest bid] const deadlineFull = sections[1].innerText?.trim(); return { name: sections[0].innerText.trim(), pid: mazyarExtractPlayerIdFromTransferMonitor(sections[0].querySelector(`a:not([class="player_icon"]`).href), deadline: 1 + Math.ceil((mazyarParseMzDateTime(deadlineFull) - new Date()) / 60_000), deadlineFull, latestBid: sections[2].querySelector(`td[align="right"]`).innerText, } }); } return []; } async #updatePlayerDeadlineFromMarket(pid) { const { player, remove } = await mazyarFetchPlayerMarketDetail(pid, this.#sport); if (player) { await this.#addPlayerToDeadlineListInIndexDb(player); } else if (remove) { await this.#removePlayerFromDeadlineList(pid); } } async #deadlineProcessPlayersInIndexedDb() { const players = await this.#fetchDeadlinePlayersFromIndexedDb(); const jobs = []; for (const player of players) { if (this.#deadlineLockAcquired) { jobs.push(this.#updatePlayerDeadlineFromMarket(player.pid)); } else if (player.deadline > 0) { this.#deadlines[player.pid] = player; } else { delete this.#deadlines[player.pid]; } } await Promise.all(jobs); } #deadlineIsLockerTabDead() { return Date.now() - Number(GM_getValue("deadline_locker_last_call", 0)) > (2 * DEADLINE_INTERVAL_SECONDS * 1000); } #deadlineUpdateLockStatus() { if (this.#deadlineLockAcquired) { // we have the lock GM_setValue("deadline_locker_last_call", Date.now()); } else { // check whether if lock is available to acquire const isLocked = GM_getValue("deadline_is_locked", false); if (isLocked) { // other instance of mazyar is already required the lock this.#deadlineLockAcquired = false; if (this.#deadlineIsLockerTabDead()) { // it seems locker tab is not alive. make it available for other tabs console.warn("locker tab is dead. release the lock."); GM_setValue("deadline_is_locked", false); } } else { // we can acquire the lock GM_setValue("deadline_is_locked", true); GM_setValue("deadline_locker_last_call", Date.now()); this.#deadlineLockAcquired = true; window.onbeforeunload = () => { // release lock GM_setValue("deadline_is_locked", false); this.#deadlineLockAcquired = false; } } } } #playDeadlineAlert() { if (this.#mustPlayBellForDeadline()) { const ding = new Audio(MAZYAR_DEADLINE_ALERT_SOUND); ding.play(); } } #deadlineUpdateIconStyle() { const deadlineTimeout = this.#getTransferDeadlineTimeout(); const deadlineIcon = document.getElementById("mazyar-deadline-overlay"); const strobe = Object.values(this.#deadlines).filter((player) => player.deadline <= deadlineTimeout).length > 0; if (strobe && deadlineIcon) { deadlineIcon.classList.add("mazyar-deadline-throb-lightgreen"); deadlineIcon.classList.remove("mazyar-hide"); this.#playDeadlineAlert(); } else { deadlineIcon.classList.add("mazyar-hide"); deadlineIcon.classList.remove("mazyar-deadline-throb-lightgreen"); } } async #updateDeadlines() { this.#deadlineUpdateLockStatus(); // remove old data this.#deadlines = {}; await this.#deadlineProcessPlayersInIndexedDb(); if (this.#deadlineLockAcquired) { const players = await this.#monitorFetchAndProcessYourBidsPlayers(); for (const player of players) { if (player.deadline > 0) { player.source = "monitor"; await this.#addPlayerToDeadlineListInIndexDb(player); } else { await this.#removePlayerFromDeadlineList(player.pid); } } } this.#deadlineUpdateIconStyle(); document.body.dispatchEvent(new CustomEvent("deadlines-updated")); } async injectTransferDeadlineAlert() { if (this.#isTransferDeadlineAlertEnabled()) { this.#addDeadlineIndicator(); await this.#updateDeadlines(); setInterval(() => { this.#updateDeadlines(); }, 1000 * DEADLINE_INTERVAL_SECONDS); } } #addDeadlineButtonToPlayerInTransferMarket(player, deadlines) { if (player.deadlineIconInjected) { return; } player.deadlineIconInjected = true; const playerId = mazyarExtractPlayerIdFromContainer(player); let alreadyAdded = deadlines.find((player) => player.pid == playerId); const addButton = mazyarCreateAddToDeadlineIcon("Deadline Monitor Add/Remove", alreadyAdded ? "red" : "green") player.querySelector("h2.clearfix div")?.appendChild(addButton); addButton.addEventListener("click", async () => { if (alreadyAdded) { alreadyAdded = false; addButton.style.color = "green"; await this.#removePlayerFromDeadlineList(playerId); } else { alreadyAdded = true; addButton.style.color = "red"; await this.#updatePlayerDeadlineFromMarket(playerId); } this.#deadlineUpdateIconStyle(); }); } #createFilterInfo(data = { name: "", scout: { high: [], low: [] }, count: "" }) { const info = document.createElement("div"); const nameSpan = document.createElement("p"); const scoutSpan = document.createElement("p"); const countSpan = document.createElement("p"); info.classList.add("mazyar-scout-filter-info"); nameSpan.innerHTML = `<strong>Filter Name: </strong>${data.name}`; scoutSpan.innerHTML = `<strong>Selected: ${this.#getSelectedScoutsOptionTextForFilter(data.scout)}</strong>`; countSpan.innerHTML = `<strong>Hit Count: </strong><span class="filter-count-span">${data.count}</span>`; info.appendChild(nameSpan); info.appendChild(scoutSpan); info.appendChild(countSpan); return info; } async #appendFilterResultToModal(middle, searchResults, filterId) { for (const result of searchResults) { const parser = new DOMParser(); const player = parser.parseFromString(result.content.players, "text/html").body.firstChild; if (this.#doesTransferNeedPlayerProfile()) { this.#updateProfileRelatedFieldsInTransfer(player); } player.id = ""; this.#fetchOrExtractPlayerScoutReport(player).then(report => { mazyarColorizeSkills(player, report); }); const a = player.querySelector("h2>div>a.subheader"); if (a) { a.href = `https://${location.hostname}/?p=transfer&u=${result.playerId}`; a.target = "_blank"; const tools = player.querySelector("td span.player_icon_placeholder.bid_button")?.parentNode; if (tools) { tools.style.display = "none"; } player.querySelector("div.floatRight.transfer-control-area")?.classList.add("mazyar-transfer-control-area"); middle.appendChild(player); } else { this.#removeHitFromIndexedDb(filterId, result.playerId); } } } async displayFilterResults(filterId, filterInfo) { this.#displayLoading("MZY Filter Results"); const players = await this.#getHitsFromIndexedDb(filterId); const jobs = []; for (const player of players) { const url = `https://${location.hostname}/ajax.php?p=transfer&sub=transfer-search&sport=${this.#sport}&u=${player.pid}`; jobs.push( mazyarFetchJson(url).then((content) => { return { playerId: player.pid, content }; }) ); } const searchResults = await Promise.all(jobs); const results = document.createElement("div"); results.classList.add("mazyar-scrollable-vertical"); results.style.flex = "1"; results.style.padding = "5px 2px"; this.#appendFilterResultToModal(results, searchResults, filterId); const body = this.#createModalBody(); if (results.childNodes.length !== 0) { const info = this.#createFilterInfo(filterInfo); info.querySelector(".filter-count-span").innerText = results.childNodes.length.toString(); body.appendChild(info); } else { results.innerHTML = "<h3>No Players To Display</h3><span>Please refresh the filter to update hits.</span>"; results.style.padding = "10px"; results.style.textAlign = "center"; } body.appendChild(results); const header = mazyarCreateMzStyledModalHeader("MZY Filter Results", () => { this.#hideModal(); this.#displayTransferFilters(); }); this.#showModal([header, body]); } async #displayPlayerComment(target, playerId, playerName, closeCallback = null) { this.#displayLoading("MZY Player Note"); const header = mazyarCreateMzStyledModalHeader("MZY Player Note", () => { this.#hideModal(); if (closeCallback) { closeCallback(); } }); const body = this.#createModalBody(); const text = document.createElement("textarea"); text.classList.add("mazyar-player-comment-textarea"); body.appendChild(text); const footer = this.#createModalFooter(); const save = mazyarCreateMzStyledButton("Save", "green"); footer.appendChild(save); text.value = await this.#fetchPlayerCommentFromIndexedDb(playerId); save.addEventListener("click", async () => { if (text.value) { await this.#setPlayerCommentInIndexedDb(playerId, text.value, playerName); target?.classList.remove("mazyar-player-comment-icon-inactive"); target?.classList.add("mazyar-player-comment-icon-active"); } else { await this.#deletePlayerCommentFromIndexedDb(playerId); target?.classList.add("mazyar-player-comment-icon-inactive"); target?.classList.remove("mazyar-player-comment-icon-active"); } this.#hideModal(); if (closeCallback) { closeCallback(); } }); this.#showModal([header, body, footer]); } async #getAllPlayerComments() { const players = await this.#db.comment.toArray() .then((players) => { return players?.filter((player) => player.sport === this.#sport); } ).catch((err) => { console.warn(err); return []; }); const jobs = []; for (const player of players) { if (!player.name) { jobs.push(this.#updatePlayerNameForCommentInIndexedDb(player.pid).then((name) => { player.name = name; })); } } await Promise.all(jobs); return players; } #commentsViewCreateTableBody(players = []) { const tbody = document.createElement("tbody"); for (const player of players) { const tr = document.createElement("tr"); const pid = document.createElement("td"); pid.innerText = player.pid; const name = document.createElement("td"); name.innerHTML = `<a target="_blank" href="https://${location.hostname}/?p=players&pid=${player.pid}">${player.name ?? player.pid}</a>`; const viewIcon = mazyarCreateCommentIcon("View the comment.", { fontSize: "1rem", margin: "0px 3px 0px 0px" }); viewIcon.style.color = "blue"; viewIcon.onclick = () => { this.#hideModal(); this.#displayPlayerComment(null, player.pid, player.name, () => { this.#displayPlayersCommentList(); }); }; const deleteIcon = mazyarCreateTrashIcon("Delete the comment.", { fontSize: "1rem", margin: "0px 3px 0px 0px" }); deleteIcon.onclick = () => { this.#deletePlayerCommentFromIndexedDb(player.pid); tbody.removeChild(tr); if (tbody.children.length === 0) { tbody.parentNode.dispatchEvent(new CustomEvent("destroy")); } }; const del = document.createElement("td"); del.appendChild(deleteIcon); const view = document.createElement("td"); view.appendChild(viewIcon); tr.appendChild(del); tr.appendChild(pid); tr.appendChild(name); tr.appendChild(view); tbody.appendChild(tr); } return tbody; } #commentsCreateTable(comments, destroyCallback) { const div = document.createElement("div"); div.classList.add("mazyar-modal-table-container"); const table = document.createElement("table"); const thead = mazyarCreateTableHeaderForCommentsView(); const tbody = this.#commentsViewCreateTableBody(comments); table.classList.add("mazyar-table", "tablesorter", "hitlist", "marker"); table.style.margin = "0.5rem"; table.appendChild(thead); table.appendChild(tbody); table.addEventListener("destroy", destroyCallback); div.appendChild(table); return div; } async #displayPlayersCommentList() { this.#displayLoading("MZY Comments"); const header = mazyarCreateMzStyledModalHeader("MZY Comments", () => { this.#hideModal(); }); const body = this.#createModalBody(); // filters table is fully shown and scrollable only when it is structured like body > div > div > table const listView = document.createElement("div"); const noListView = document.createElement("span"); body.appendChild(listView); body.appendChild(noListView); listView.classList.add("mazyar-flexbox-column"); noListView.innerText = "There is no comment to display"; noListView.style.display = "none"; noListView.style.margin = "1rem"; const comments = await this.#getAllPlayerComments(); if (comments.length > 0) { const deleteAll = mazyarCreateDeleteAllFiltersButton("Delete all comments", async () => { await this.#deleteAllComments(); listView.style.display = "none"; noListView.style.display = "unset"; }); const table = this.#commentsCreateTable(comments, () => { // remove 'delete all' button if no filter is left listView.style.display = "none"; noListView.style.display = "unset"; }); listView.appendChild(deleteAll); listView.appendChild(table); } else { noListView.style.display = "unset"; } this.#showModal([header, body]); } #getVersionNumbers(v) { return v.split('.').map((i) => Number(i)); } #getVersionsOfChangelog(MAZYAR_CHANGELOG) { return Object.keys(MAZYAR_CHANGELOG).map((v) => this.#getVersionNumbers(v)); } #isVersionLesserThan(version = [0, 0], base = [0, 0]) { return version[0] < base[0] || (version[0] === base[0] && version[1] < base[1]); } #isVersionGreaterThan(version = [0, 0], base = [0, 0]) { return version[0] > base[0] || (version[0] === base[0] && version[1] > base[1]); } #showChangelog(force = false) { const previousVersion = GM_getValue("previous_version", ""); if (!previousVersion) { GM_setValue("previous_version", CURRENT_VERSION); return; } const previous = this.#getVersionNumbers(previousVersion); const current = this.#getVersionNumbers(CURRENT_VERSION); if (!force && !this.#isVersionLesserThan(previous, current)) { return; } const headHTML = ` <span><b>Mazyar</b> is updated</span> <br><span>from <b style="color: red;">v${previousVersion}</b> <span> to </span><b style="color: blue;">v${CURRENT_VERSION}</b></span>`; const head = document.createElement("div"); head.innerHTML = headHTML; head.style.textAlign = "center"; const changesTitle = document.createElement("div"); changesTitle.innerHTML = `<b>Changelog:</b>`; changesTitle.style.margin = "5px"; changesTitle.style.width = "100%"; changesTitle.style.width = "100%"; changesTitle.style.textAlign = "left"; let changesHTML = ''; const versions = this.#getVersionsOfChangelog(MAZYAR_CHANGELOG); for (const version of versions) { if (this.#isVersionGreaterThan(version, current)) { continue; } if (this.#isVersionGreaterThan(version, previous)) { const v = version.join('.'); changesHTML += ` <div style="margin-bottom: 1rem;"> <b>v${v}</b> <ul style="margin: 0px 5px 5px;"> <li>${MAZYAR_CHANGELOG[v]?.join("</li><li>")}</li> </ul> </div>`; } } if (changesHTML === '' && force) { const v = versions[0].join('.'); changesHTML += ` <div style="margin-bottom: 1rem;"> <b>v${v}</b> <ul style="margin: 0px 5px 5px;"> <li>${MAZYAR_CHANGELOG[v]?.join("</li><li>")}</li> </ul> </div>`; } const changes = document.createElement("div"); changes.innerHTML = changesHTML; changes.style.maxHeight = "320px"; changes.style.maxWidth = "320px"; changes.style.paddingRight = "30px"; changes.style.backgroundColor = "khaki"; changes.style.padding = "5px"; changes.style.flex = "1"; changes.classList.add("mazyar-scrollable-vertical"); const body = this.#createModalBody(); body.style.margin = "10px"; body.style.padding = "5px"; // changes has scroll for itself. if it is not unset here (from auto), it would add scroll on x-axis body.style.overflow = "unset"; body.appendChild(head); body.appendChild(changesTitle); body.appendChild(changes); const header = mazyarCreateMzStyledModalHeader("MZY Notice", () => { GM_setValue("previous_version", CURRENT_VERSION); this.#hideModal(); }); this.#showModal([header, body]); } showPlayerInModal(playerView) { const header = mazyarCreateMzStyledModalHeader("MZY Player View", () => { this.#hideModal(); }); const body = this.#createModalBody(); const player = document.createElement("div"); player.appendChild(playerView); body.appendChild(player); player.style.margin = "5px"; player.style.padding = "0px"; player.style.backgroundColor = "wheat"; this.#showModal([header, body]); } #mergeAndKeepFilters(filters) { const hockey = mazyarMergeFilters(filters.hockey, this.#filters.hockey); const soccer = mazyarMergeFilters(filters.soccer, this.#filters.soccer); this.#filters = JSON.parse(JSON.stringify({ hockey, soccer })); } #mergeAndReplaceFilters(filters) { const hockey = mazyarMergeFilters(this.#filters.hockey, filters.hockey); const soccer = mazyarMergeFilters(this.#filters.soccer, filters.soccer); this.#filters = JSON.parse(JSON.stringify({ hockey, soccer })); } #createExportButtons(modal) { const filtersButton = mazyarCreateMzStyledButton("MZY Filters", "blue", "floatLeft"); filtersButton.addEventListener("click", () => { const notepad = modal.querySelector("textarea.calendar_textarea"); const filters = btoa(JSON.stringify(this.#filters, null, 0)); notepad.value = MAZYAR_FILTER_BACKUP_TITLE + filters; }); return [filtersButton,]; } #showImportFiltersModal(filters) { const header = mazyarCreateMzStyledModalHeader("MZY Import Filters", () => { this.#hideModal(); }); const body = this.#createModalBody(); const overviewOld = mazyarCreateFiltersOverview(this.#filters, "Current Filters", "black"); const overviewNew = mazyarCreateFiltersOverview(filters, "New Filters", "crimson"); body.appendChild(overviewOld); body.appendChild(overviewNew); const footer = this.#createModalFooter(); const keep = mazyarCreateMzStyledButton("Merge & Keep", "blue"); const replace = mazyarCreateMzStyledButton("Merge & Replace", "red"); footer.appendChild(keep); footer.appendChild(replace); keep.addEventListener("click", () => { this.#mergeAndKeepFilters(filters); this.#hideModal(); }); replace.addEventListener("click", () => { this.#mergeAndReplaceFilters(filters); this.#hideModal(); }); this.#showModal([header, body, footer]); } #attachImportButton(note) { if (note.value.startsWith(MAZYAR_FILTER_BACKUP_TITLE)) { const button = mazyarCreateMzStyledButton("MZY Import Filters", "blue", "floatLeft"); button.style.marginTop = "10px"; button.addEventListener("click", () => { try { const filters = JSON.parse(atob(note.value.replace(MAZYAR_FILTER_BACKUP_TITLE, ""))); this.#showImportFiltersModal(filters); } catch (e) { console.log(e); } }); note.parentNode.appendChild(button); } } injectToCalendar(modal) { const addButton = document.getElementById("calendar_add_not_btn"); if (addButton) { const exportButtons = this.#createExportButtons(modal); addButton.parentNode.append(...exportButtons); } else { const createButton = document.getElementById("create_note_btn"); if (createButton) { createButton.addEventListener("click", () => { const addButton = document.getElementById("calendar_add_not_btn"); if (addButton) { const exportButtons = this.#createExportButtons(modal); addButton.parentNode.append(...exportButtons); } }); const notes = modal.querySelectorAll("textarea.calendar_textarea_disabled"); for (const note of notes) { this.#attachImportButton(note); } } } } } /* *********************** Database ********************************** */ async function initializeIndexedDb(name = "db") { const db = new Dexie(name); db.version(1).stores({ comment: "[sport+pid]", scout: "[sport+pid]", hit: "[fid+pid],deadline", filter: "&fid,totalHits,scoutHits,lastCheck", }); db.version(2).stores({ scout: "[sport+pid],ts", player: "[sport+pid],ts,maxed,days,camp", hide: "[sport+pid],ts", deadline: "[sport+pid],ts,deadline,name,deadlineFull,latestBid,source,flag", comment: "[sport+pid],comment,name", }).upgrade(trans => { return trans.table("scout").toCollection().modify(report => { report.ts = 0; }); }); db.open(); // remove outdated data from db const scoutOutdate = Date.now() - 7 * 24 * 60 * 60 * 1000; await db.scout.where("ts").below(scoutOutdate).delete().then((deleteCount) => { console.log("Deleted " + deleteCount + " outdated scout reports"); }).catch((error) => { console.warn(error); }); // remove outdated data from db const startOfDay = Date.now() - Date.now() % (24 * 60 * 60 * 1000); await db.player.where("ts").below(startOfDay).delete().then((deleteCount) => { console.log("Deleted " + deleteCount + " outdated player profile."); }).catch((error) => { console.warn(error); }); const info = await navigator?.storage?.estimate(); if (info) { console.log("ManagerZone IndexedDB size: " + mazyarFormatFileSize(info.usage)); } return db; } /* *********************** Inject ********************************** */ function isVisitingTeamPage() { const regex = /^\?((p=team)|(p=team&tid=\d+))$/g; return regex.test(document.location.search); } async function inject() { const styles = GM_getResourceText("MAZYAR_STYLES"); GM_addStyle(styles); const db = await initializeIndexedDb("Mazyar"); mazyar = new Mazyar(db); if (mazyar.isTransferFiltersEnabled()) { mazyar.setInitialFiltersHitInToolbar(); } mazyar.injectTransferDeadlineAlert(); calendarInject(); const uri = document.baseURI; if (uri.search("/?p=federations") > -1) { if (uri.search("&sub=clash") > -1) { clashInjectRanks(); } else if (uri.search("&fid=") > -1 || uri.endsWith("p=federations")) { federationFrontPage(); } } else if (uri.search("/?p=players") > -1) { // owned by legacy-player-advanced // owned by legacy-player-advanced } else if (uri.search("mid=") > -1) { // owned by legacy-player-advanced } else if (uri.search("/?p=match") > -1) { if (uri.search("&sub=result") < 0) { // owned by mzone-advanced } if (uri.endsWith("&sub=scheduled") > -1 && mazyar.mustAddFullNamesToFixture()) { fixtureChangeNames(); } } else if (uri.search("/?p=league") > -1) { // owned by mzone-advanced // owned by mzone-advanced } else if (uri.search("/?p=friendlyseries") > -1) { // owned by mzone-advanced } else if (uri.search("/?p=cup&") > -1 || uri.search("/?p=private_cup&") > -1) { // owned by mzone-advanced } else if (uri.search("/?p=transfer") > -1) { if (uri.search("/?p=transfer&sub=yourplayers") > -1) { mazyar.monitorInject(); } else { if (mazyar.isTransferFiltersEnabled()) { transferInject(); } mazyar.executeTransferTasks(); } } else if (uri.search("/?p=clubhouse") > -1) { if (mazyar.mustHelpWithPredictor()) { predictorInject(); } } else if (isVisitingTeamPage()) { addTrophyCountToClubPage(); } else if (uri.search("/?p=trainers") > -1) { if (mazyar.mustAddCoachSalaries()) { trainersAddRequestedSalaries(); } } else if (uri.search("/?p=training_report") > -1) { trainingAddCampOpenerToReport(); trainingAddDays(); } else if (uri.search("/?p=rank&sub=userrank") > -1) { rankingInjectSquadValue(); } else if (uri.search("/?p=shortlist") > -1) { shortlistAddDays(); } else if (uri.search("/?p=tactics") > -1) { tacticAddDays(); } } if (document.readyState === "loading") { // Loading hasn't finished yet document.addEventListener("DOMContentLoaded", () => { inject(); }); } else { // `DOMContentLoaded` has already fired inject(); } })(); }).call(window, __mzcDexie4, __mzcJQuery, __mzcJQuery); }); /* mzcopilot:module:[email protected] */ window.__mzcMega.run("mzone-advanced", "18.0", function () { (function (pako, Dexie, $, jQuery) { (async function () { 'use strict'; const $ = jQuery; // --- Sport Detection (Soccer Only) --- const isHockeyMode = () => { // 1. Logo Metni Kontrolü (Hokey/Hockey ibaresine bakar) const logo = document.getElementById('mz_logo'); if (logo) { const text = (logo.innerText || logo.textContent || "").trim().toLowerCase(); if (text === 'hokey' || text === 'hockey') return true; if (text === 'futbol' || text === 'soccer') return false; } // 2. Spor Değiştirme Butonu (Üst menüdeki link) // Eğer sayfada "Futbol'a geç" linki Varsa, şu an HOCKEY'deyizdir. const toSoccer = document.querySelector('a[href*="changesport=soccer"]'); const toHockey = document.querySelector('a[href*="changesport=hockey"]'); if (toSoccer && !toHockey) return true; if (toHockey) return false; // 3. URL Parametre Kontrolü const params = new URLSearchParams(window.location.search); if (params.get('sport') === 'hockey') return true; if (params.get('sport') === 'soccer') return false; // Varsayılan: Futbol sayfasında çalışma ihtimalini bozmamak için false döner return false; }; if (isHockeyMode()) { console.log("[MZone Advanced] Hockey mode detected. Script is disabled for this sport."); return; } const mzIsMobile = (function () { if (navigator.userAgentData && navigator.userAgentData.mobile) return true; const ua = (navigator.userAgent || navigator.vendor || window.opera).toLowerCase(); const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(ua); // Genişliği 900 yaptık (Telefonlar ve küçük tabletler için ideal). return isMobileUA || (window.innerWidth <= 900); })(); // CSS to constrain Van.mz skill ball images to prevent layout overflow GM_addStyle(` /* Skill ball dimensions matching original MZone (width: 126px, height: 10px) */ div.skill img, div.skilladd img, .player-skills img.skill, .player-skills img.ignore-fluid { max-height: 10px !important; height: 10px !important; width: auto !important; } /* Ensure skill containers don't expand, BUT exclude our training percentage containers */ div.skill:not(.skill_exact2), div.skilladd:not(.skill_exact2) { line-height: 1 !important; font-size: 0 !important; } /* Training percentage text - matching Van.mz style (lighter, normal weight) */ .skill_exact2, .skill_exact2 * { font-size: 11px !important; } .skill_exact_van { font-weight: normal !important; color: #666 !important; font-size: 11px !important; } /* Van.mz style replication */ .skill_exact2, .skill_exact2 * { font-size: 11px !important; } /* Keep gap reduction if user still wants it, otherwise Van.mz specific styling is priority */ td.skill_exact { padding-left: 2px !important; } td.skillval { padding-right: 2px !important; } `); // Layout adjustments for Player Profile (Only on players page to avoid clubhouse distortion) if (window.location.href.includes('p=players')) { GM_addStyle(` .player-image { margin-left: -45px !important; } .skills-container { margin-left: 15px !important; } .fire_player { position: relative !important; left: -40px !important; } `); } // Layout adjustments for Training Report Page, Training Camp Page, and Shortlist Page (User Request) // Player modal geometry is owned by ManagerZone; do not offset it. // --- Global Utility Functions --- function debounce(func, wait) { let timeout; return function (...args) { const context = this; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), wait); }; } /* === CENTRAL CORE MODULE === */ const Core = { settings: {}, i18n: { // Early jQuery ($) dependence removed to prevent crashes before masterRouter lang: (function () { const meta = document.querySelector('meta[name="language"]'); return (meta && meta.getAttribute('content') === 'tr') ? 'tr' : 'en'; })(), TRANSLATIONS: { tr: { // --- Genel/Ortak --- menuTitle: "MZ Gelişmiş Bilgi Paneli", dragToMove: "Taşımak için sürükleyin", settings: "Ayarlar", selectAll: "Hepsini Seç", deselectAll: "Hepsini Kaldır", saveSettings: "Ayarları Kaydet", reloadToApply: "Değişikliklerin etkili olması için sayfayı yenilemeniz gerekebilir.", featureSettingsTitle: "Modül Açma/Kapama", sendMessageToAuthor: "Yapımcıya mesaj gönder", all: "Hepsi", none: "Hiçbiri", league: "Lig", under18: "18 Yaş Altı", under21: "21 Yaş Altı", under23: "23 Yaş Altı", statistics: "İstatistikler", player: "Oyuncu", age: "Yaş", matches: "Maçlar", goal: "Gol", assist: "Asist", yellowCard: "Sarı Kart", redCard: "Kırmızı Kart", close: "Kapat", processing: "İşleniyor...", error: "Hata:", warning: "Uyarı:", info: "Bilgi:", success: "Başarılı!", confirm: "Onayla", cancel: "İptal", delete: "Sil", save: "Kaydet", reset: "Sıfırla", update: "Güncelle", loading: "Yükleniyor...", passed: "Geçti", nextSeason: "Gelecek Sezon", minTaxReached: "Minimum vergi oranına ulaşıldı (%15).", option: "Seçenek", // --- Bölüm 1: Lig Tablosu --- averageValue: "Ortalama Değer", totalValue: "Toplam Değer", top11Value: "İlk 11 Değeri", leagueAverages: "Lig Ortalamaları", leagueTotal: "Lig Toplamı", teamValue: "Kadro Değeri", rank: "Sıra", points: "Puan", goalsFor: "AG", goalsAgainst: "YG", average: "Avr", leagueTitle: "Gelişmiş Lig Tablosu", leagueDesc: "Canlı skorlar, fikstür zorluğu ve ELO puanlarını gösterir.", // --- Bölüm 2: Oyuncu İstatistikleri --- compareAllPlayers: "Tüm Oyuncuları Karşılaştır", playerComparison: "Oyuncu İstatistik Karşılaştırması", exportToExcel: "Excel'e Aktar", exporting: "Aktarılıyor...", exportError: "Excel aktarımı başarısız.", backupInProgress: "Yedekleniyor...", restoreSuccess: "Geri yükleme başarılı!", restoreError: "Geri yükleme hatası!", noMatchesForCriteria: "Kriterlere uygun maç bulunamadı.", avgStatsForMatches: (count) => `${count} Maç İçin Ortalama İstatistikler`, playersTitle: "Oyuncu İstatistikleri", playersDesc: "Oyuncularınızın maç istatistiklerini toplar ve analiz eder.", // --- Bölüm 3: Playoff Tahmincisi --- playoffPredictorTitle: "Muhtemel Play-Off / Play-Out Grupları", predictorDisclaimer: "Not: Bu tablolar mevcut puan durumuna göre bir tahmindir, resmiyet taşımaz.", fetchingLeagueData: (current, total, league) => `Lig Verileri Çekiliyor... (${current}/${total}) - ${league}`, calculatingTop11: (current, total) => `İlk 11 Değerleri Hesaplanıyor... (${current}/${total})`, retireWarning: "Emeklilik Yakın!", canExtendRetirement: "Kariyeri Uzatılabilir", // --- Bölüm 4: Takip Listesi Filtresi --- shortlistFilterTitle: "Oyuncuları Filtrele", minAge: "Min Yaş", maxAge: "Max Yaş", retiringOnly: "Emekli Olacaklar", onTransferOnly: "Transferde (Aktif)", expiredTransferOnly: "Süresi Dolanlar", nationality: "Milliyet", national: "Yerli", foreign: "Yabancı", playersFiltered: (count) => `${count} oyuncu filtrelendi.`, downloadImageTitle: "Oyuncu profilini resim olarak indir", downloading: "İndiriliyor...", shortlistTitle: "Gelişmiş Takip Listesi", shortlistDesc: "Piyasa takipçisi, gelişmiş filtreleme ve profil resmi indirme araçlarını sunar.", // --- Bölüm 5: Vergi & Transfer Geçmişi --- trackPlayer: "Transfer Geçmişini İzle", historyFor: "Transfer Geçmişi:", noHistory: "Kayıtlı transfer geçmişi bulunmuyor.", clearHistory: "Geçmişi Temizle", confirmClearHistory: "Bu oyuncunun tüm transfer geçmişini silmek istediğinizden emin misiniz?", statusListed: "Satışa Çıktı", statusSold: "Satıldı", statusExpired: "Süresi Doldu", statusBid: "Teklif Verildi", calcTitle: "Vergi İndirimi Hesaplayıcısı", lastTransferDate: "Son Transfer Tarihi", daysElapsed: "Geçen Süre", currentTax: "Mevcut Vergi", days: "gün", taxDrop50: "%50'ye Düşüş", taxDrop15: "%15'e Düşüş", originalPlayer: "İlk/Altyapı Oyuncusu", originalNote: "Vergi sadece yaşa bağlıdır.", taxDrop20: "%20'ye Düşüş (20 Yaş)", taxDrop15_age: "%15'e Düşüş (21 Yaş)", // --- Bölüm 7: İstatistik Özeti --- summarizeSeasons: "Bu Sekmeyi Özetle", summarizeAll: "TÜM SEKMELERİ ÖZETLE", clearCache: "Sıfırla", clearTrainingCache: "Tüm Antrenman Önbelleğini Temizle", updateCurrentTab: "Tüm Son Sezonları Güncelle", lastSeasonUpdateComplete: "Tüm bölümlerin son sezonları güncellendi!", summaryTitle: "Sezon Performans Özeti", fetchingSeason: (season, current, total) => `${season}. sezon çekiliyor... (${current}/${total})`, chartTitle: (metric) => `Sezonlara Göre ${metric} Geçmişi`, statisticsTitle: "Sezon Performans Özeti", statisticsDesc: "Geçmiş sezonlardaki lig sırası ve puan değişimlerini grafiklerle sunar.", // --- Bölüm 10: Gelişmiş Maç Bulucu --- matchFinderTitle: "Gelişmiş Maç Bulucu", minRank: "Min Sıralama", maxRank: "Max Sıralama", minValue: "Min Değer", maxValue: "Max Değer", scanLeagues: "Ligleri Tara", searchingTeams: (current, total) => `Takımlar Aranıyor... (${current}/${total})`, matchFinderName: "Gelişmiş Maç Bulucu", resultsToolsTitle: "Gelişmiş Maç Bulucu", resultsToolsDesc: "Rakip takımların geçmiş maçlarını ve taktiklerini bulur.", // --- Bölüm 14: Hazırlık Maçı Otomasyonu --- friendlyMatchAutoTitle: "Otomatik Hazırlık Maçı Robotu", friendlyMatchAutoDesc: "Belirlenen günlerde, ayarlanan taktiklerle otomatik hazırlık maçı teklifi gönderir.", panelTitle: "MZ Otomatik Hazırlık Maçı Teklifi", autoModeLabel: "OTOMATİK MOD (6 SAATTE BİR)", settingsToggle: "🛠️ Taktik Ayarları", startBtn: "MANUEL BAŞLAT", stopBtn: "DURDUR", logOfferSent: (user, day) => `> ${user}: ${day} İÇİN TEKLİF GÖNDERİLDİ.`, dayNames: ["Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"], modalHelper: "Sadece maç oynanan 5 gün listelenir:", btnSelectAll: "Hepsini Seç", btnSave: "Kaydet", btnCancel: "İptal", settingsTitle: "Otomasyon Ayarları", saveButton: "Ayarları Kaydet", resetButton: "Ayarları Sıfırla", closeButton: "Kapat", colOfferDay: "Teklif Günü", colMatchDay: "Maç Günü", colActive: "Aktif", colTacticHome: "İç Saha Taktik", colTacticAway: "Dış Saha Taktik", inputPlaceholder: "Kullanıcı adı...", addBtn: "Ekle", addFriendsBtn: "Arkadaşları Çek", listHeader: "Seçili Olanlara Gönderilir:", fmSelectAll: "Tümü", fmDeleteSelected: "Sil", addedNewUsers: (count) => `${count} yeni kişi eklendi.`, emptyList: "Liste boş. İsim ekleyin veya arkadaşları çekin.", alertSaved: "Ayarlar Kalıcı Olarak Kaydedildi!", alertSaveError: "Kaydederken hata oluştu: ", alertResetConfirm: "Tüm ayarlar silinecek. Onaylıyor musunuz?", settingsNote: "Değişiklik yaptıktan sonra KAYDET butonuna basmayı unutmayın.", confirmDelete: (count) => `${count} adet kullanıcı kalıcı olarak silinecek. Emin misiniz?`, noSelection: "Kimse seçilmedi!", noBuddyList: "Arkadaş listesi açık değil.", logSystemStart: (count) => `> SİSTEM BAŞLATILDI. KUYRUK: ${count} KİŞİ`, logRobotCall: `> ARKA PLAN ROBOTU ÇAĞRILIYOR...`, logSearch: (user) => `> ${user} ARANIYOR...`, logNotFound: (user) => `> ${user} BULUNAMADI, GEÇİLİYOR.`, logNoDay: (user) => `> ${user}: UYGUN GÜN YOK (AYARLARA BAKIN).`, logDayOff: "> GÜNLÜK AYAR KAPALI.", logErrorTimeout: '> HATA: Sayfa zaman aşımı.', logProcessDone: (user, remaining) => `> ${user} İŞLEMİ BİTTİ. KALAN: ${remaining}`, logAllComplete: "> TÜM GÖREVLER TAMAMLANDI. 6 SAAT SONRA TEKRARLANACAK. ✅", // --- Bölüm 16: Hızlı Transfer Tarayıcı (TSF) --- tsfBtnTitle: "Gözlemci Robotu", tsfModalTitle: "Hızlı Transfer Tarayıcı", tsfStartMsg: "Transfer listesini tarar, oyuncuların YP (¹) ve DP (²) özelliklerini analiz eder.", tsfMinStarLabel: "En Az Kaç Yıldız (YP) Kaydedilsin:", tsfStarInfo: "Sadece 2, 3 veya 4 yazabilirsiniz.", tsfBtnStart: "TARAMAYI BAŞLAT", tsfBtnClose: "Kapat", tsfBtnStop: "DURDUR", tsfScanningMsg: "Hızlı Tarama ve Analiz Yapılıyor... ", tsfWaitMsg: "Yükleniyor...", tsfPageInfo: "İşlenen Sayfa: ", tsfFoundInfo: "Bulunan Oyuncu: ", tsfFinishMsg: "Tarama Tamamlandı!", tsfPanelTitle: "FİLTRELEME PANELİ", tsfLblHP: "Yüksek Potansiyel (YP)", tsfLblLP: "Düşük Potansiyel (DP)", tsfLblSpeed: "Hız (AH)", tsfLblUnscouted: "Gözlemlenmemiş", tsfCountVisible: "Filtrelenen Sonuç: ", tsfPrevPage: "« Geri", tsfNextPage: "İleri »", tsfPageOf: "Sayfa", tsfStoppedMsg: "Durduruldu.", tsfViewModeCard: "Kart Görünümü", tsfViewModeTable: "Tablo Görünümü", tsfToggleFilters: "Detaylı Potansiyel Filtresi ▼", tsfFilterYP: "YP", tsfFilterNotYP: "YP Yok", tsfFilterNotDP: "DP Yok", tsfPresetLabel: "Kayıtlı Ayarlar:", tsfBtnSavePreset: "Kaydet", tsfBtnLoadPreset: "Yükle", tsfBtnDeletePreset: "Sil", tsfPhPresetName: "Ayar İsmi...", tsfAlertPresetSaved: "Ayarlar başarıyla kaydedildi!", tsfAlertPresetLoaded: "Ayarlar yüklendi!", tsfAlertPresetDeleted: "Ayar silindi.", tsfAlertEnterName: "Lütfen bir ayar ismi giriniz.", transferScoutFilterTitle: "Gözlemci Robotu", transferScoutFilterDesc: "Transfer listesini Yüksek/Düşük Potansiyel yıldızlarına göre filtreler.", // --- Diğer Modüller --- transferTrackerTitle: "Transfer Geçmişi İzleyici", transferTrackerDesc: "Oyuncu transfer geçmişini takip eder ve grafiklerle sunar.", fixturePageEloTitle: "Fikstür Sayfası ELO Puanları", fixturePageEloDesc: "Fikstür sayfasında rakip takımların ELO puanlarını görüntüler.", maxedBallTitle: "Maxlanmış Top Renklendirme", maxedBallDesc: "Maksimum seviyeye ulaşmış yetenekleri renklendirir.", fixtureToolsTitle: "Rakiplerin Geçmiş Maçlarını Analiz Et", fixtureToolsDesc: "Fikstür listesindeki takımların ELO puanlarını gösterir.", rankingTweaksTitle: "Menajer Sıralaması İyileştirmeleri", rankingTweaksDesc: "Sıralama sayfasından hızlıca gözlemci maçı teklif etmenizi sağlar.", rankingScoutTitle: "Gözlemci maçı teklif et", ghostRobotTitle: "Duran Top Kopyalayıcı", ghostBtnTitle: "Duran Top Kopyala", ghostRobotDesc: "Duran top organizasyonlarını bir taktikten diğerlerine saniyeler içinde kopyalar.", ghostLogReady: "[MZ GHOST] Hazır.", ghostLogStart: "--- İŞLEM BAŞLADI ---", ghostErrorNoXML: "Kaynak XML alınamadı.", ghostErrorNoPlayer: "Kaynak taktikte oyuncu bulunamadı.", ghostErrorNoTarget: "Kopyalanacak başka taktik yok.", ghostConfirmMsg: (tabName, count) => `"${tabName}" taktiğindeki sıralama alınıp, ${count} adet taktiğe kopyalanacak.\n\nİşlem bitene kadar bekleyin.\n\nBaşlasın mı?`, ghostSuccessMsg: "İŞLEM BAŞARILI!\n\nLütfen aşağıdaki 'HEPSİNİ KAYDET' butonuna basarak işlemi bitirin.", ghostProcessing: "İşleniyor...", linkEnhancementsTitle: "Link İyileştirmeleri", linkEnhancementsDesc: "Lig sayfalarındaki takım linklerini düzeltir ve genç maçlarına eksik lig linklerini ekler.", messengerToolsTitle: "Messenger Toplu Silme", messengerToolsDesc: "Mesaj kutusundaki sohbetleri toplu olarak silmenizi sağlar.", messengerSelectAll: "Tümü Seç", messengerDeselectAll: "Tümü Kaldır", messengerDeleteSelected: "Seçilenleri Sil", messengerConfirmDelete: (count) => `${count} adet sohbet kalıcı olarak silinecek. Emin misiniz?`, messengerDeleting: (current, total) => `Siliniyor: ${current}/${total}...`, messengerDeletionComplete: "Silme işlemi tamamlandı.", messengerNoSelection: "Lütfen silmek için en az bir sohbet seçin.", copyHome: 'XML Kopyala (Ev)', copyAway: 'XML Kopyala (Deplasman)', copied: 'Taktik XML panoya kopyalandı!', copyError: 'XML kopyalama başarısız.', rBtnTitle: 'Maç sonucunu ve taktikleri göster', errMsgNoData: 'Maç verisi mevcut değil veya alınamadı (5 deneme yapıldı).', matchDataFail: (status) => `Maç verisi hazırlanamadı (durum: ${status}). Bu maç için XML veri üretilemiyor olabilir.`, // --- Antrenman Yüzdesi --- trainingPercentageTitle: "Antrenman Yüzdesi Gösterimi", trainingPercentageDesc: "Oyuncu profillerinde son toptan bu yana olan ilerleme yüzdesini otomatik gösterir.", trainingAvg: "Ortalama", trainingTotal: "Toplam", trainingBallDay: "Tahmini kalan gün", trainingNow: "Mevcut ilerleme", trainingBall: "↑", residencyTitle: "İkamet ve Transfer Bilgisi", residencyDays: "Kulüpteki Gün Sayısı", transferFee: "Transfer Ücreti", entireCareer: "Tüm Kariyer", // --- Genel ve Taktikler --- scoutMatch: "Gözlemci maçı", playMatchNow: "Hemen Maç Yap", load: "Yükle", copy: "Kopyala", paste: "Yapıştır", rival: "Rakip", location: "Konum", roleGK: "KI", roleDef: "Defans", roleMid: "Orta", roleFor: "Forvet", tacticWings: "Kanatları Kullan", tacticShortPass: "Kısa Pas", tacticLongBall: "Uzun Top", styleOffensive: "Ofansif", styleNormal: "Normal", styleDefensive: "Defansif", aggrAggressive: "Agresif", aggrNormal: "Normal", aggrPassive: "Pasif", // --- Yetenekler --- skillSpeed: "Hız", skillStamina: "Dayanıklılık", skillIntelligence: "Oyun zekası", skillPassing: "Paslaşma", skillShooting: "Şut çekme", skillHeading: "Kafa vuruşu", skillKeeping: "Kalecilik", skillControl: "Top kontrolü", skillTackling: "Top çalma", skillCrossing: "Orta yapma", skillSetPlays: "Duran top", // --- Antrenman / Gözlemci Modal --- genelVerim: "Genel Verim", birimGun: "Birim/G", updateTraining: "Antrenman verilerini güncelle", rivalFormationTitle: "🕵️ Rakip Formasyon Yükle", matchIdInstruction: "Maçlar sayfasından aldığın Maç ID'sini gir", matchIdExample: "Örn: 1486585817", trainOrder: "Antrenman sırası:", loserMsg: "Kritik özellikler çok düşük — satmayı veya kadrodan çıkarmayı düşün.", sugTitle: "Antrenman Önerisi", posCB: "Defans", posGK: "Kaleci", posWinger: "Kanat", posStriker: "Santrafor", posMF: "Orta Saha", posPivot: "Pivot", posDM: "Defansif OS", posWB: "Bek", posAMF: "Ofansif OS", statMappings: { 'N': 'Rating', 'MP': 'Maç', 'G': 'Gol', 'A': 'Asist', 'S': 'Şut', 'G%': 'Gol %', 'SOT': 'K.Şut', 'SOT%': 'İsabet %', 'P%': 'Pas %', 'PL': 'K.Pas', 'In': 'Top Kesme', 'T%': 'T.Çalma %', 'Pos%': 'Topla Oyn.', 'Dist': 'Mesafe', 'DistP': 'Mes/Pas', 'SpP': 'Hız/Pas', 'SpR': 'Hız/Koşu', 'YC': 'Sarı K.', 'RC': 'Kırmızı K.', 'SV': 'Kurtarış' }, rivalMatchTitle: "🕵️ Rakip Takımı Seç", matchId: "Maç ID", rivalLoadError: "Maç verisi alınamadı. Maç henüz sunucuya düşmemiş olabilir.", rivalDataRequest: "⚠️ Veri bulunamadı. Sunucudan hazırlanması isteniyor...", totalValueLabel: "Toplam Değer:", formationCopySuccess: "✅ Formasyon panoya kopyalandı!", formationPasteSuccess: "✅ Formasyon uygulandı!", formationError10Players: "⚠️ Sahada tam 10 oyuncu olmalı!", formationErrorClipboardEmpty: "⚠️ Pano boş.", formationErrorInvalidFormat: "⚠️ Yapıştırılacak içerik 10 satır koordinat veya XML olmalı.", formationErrorNoCoords: "⚠️ 10 oyuncu konumu bulunamadı.", formationErrorClipboardFail: "❌ Pano erişimi başarısız.", formationErrorPasteFail: "❌ Pano okunamadı. Tarayıcı iznini kontrol et.", dragToMoveTooltip: "Sürüklemek için basılı tutun", tacticAutoFilled: "✅ Taktik otomatik dolduruldu:", tacticEnterManually: "⚠️ Manuel girin:" }, en: { // --- Global/Common --- menuTitle: "MZ Advanced Info Panel", dragToMove: "Drag to move", settings: "Settings", selectAll: "Select All", deselectAll: "Deselect All", saveSettings: "Save Settings", reloadToApply: "You may need to refresh the page for changes to take effect.", featureSettingsTitle: "Module Activation/Deactivation", sendMessageToAuthor: "Send message to author", all: "All", none: "None", league: "League", under18: "Under 18", under21: "Under 21", under23: "Under 23", statistics: "Statistics", player: "Player", age: "Age", matches: "Matches", goal: "Goal", assist: "Assist", yellowCard: "Yellow Card", redCard: "Red Card", close: "Close", processing: "Processing...", error: "Error:", warning: "Warning:", info: "Info:", success: "Success!", confirm: "Confirm", cancel: "Cancel", delete: "Delete", save: "Save", reset: "Reset", update: "Update", loading: "Loading...", passed: "Passed", nextSeason: "Next Season", minTaxReached: "Minimum tax rate reached (15%).", option: "Option", // --- Section 1: League Table --- averageValue: "Average Value", totalValue: "Total Value", top11Value: "Top 11 Value", leagueAverages: "League Averages", leagueTotal: "League Total", teamValue: "Team Value", rank: "Rank", points: "Points", goalsFor: "GF", goalsAgainst: "GA", average: "Average", leagueTitle: "Advanced League Table", leagueDesc: "Live scores, fixture difficulty, and ELO ratings.", // --- Section 2: Player Stats --- compareAllPlayers: "Compare All Players", playerComparison: "Player Stats Comparison", exportToExcel: "Export to Excel", exporting: "Exporting...", exportError: "Excel export failed.", backupInProgress: "Backing up...", restoreSuccess: "Restore successful!", restoreError: "Restore error!", noMatchesForCriteria: "No matches found for criteria.", avgStatsForMatches: (count) => `Average Stats for ${count} Matches`, playersTitle: "Player Statistics", playersDesc: "Collect and analyze your players' match statistics.", // --- Section 3: Playoff Predictor --- playoffPredictorTitle: "Potential Play-Off / Play-Out Groups", predictorDisclaimer: "Note: These tables are a prediction based on current standings and are not official.", fetchingLeagueData: (current, total, league) => `Fetching League Data... (${current}/${total}) - ${league}`, calculatingTop11: (current, total) => `Calculating Top 11 Values... (${current}/${total})`, retireWarning: "Retirement Near!", canExtendRetirement: "Career Can Be Extended", // --- Section 4: Shortlist Filter --- shortlistFilterTitle: "Filter Players", minAge: "Min Age", maxAge: "Max Age", retiringOnly: "Retiring", onTransferOnly: "On Transfer (Active)", expiredTransferOnly: "Expired Transfers", nationality: "Nationality", national: "National", foreign: "Foreign", playersFiltered: (count) => `${count} players filtered.`, downloadImageTitle: "Download player profile as image", downloading: "Downloading...", shortlistTitle: "Advanced Shortlist", shortlistDesc: "Offers market tracker, advanced filtering, and player profile download tools.", // --- Section 5: Transfer Tracker --- trackPlayer: "Track Transfer History", historyFor: "Transfer History for", noHistory: "No transfer history recorded.", clearHistory: "Clear History", confirmClearHistory: "Are you sure you want to delete all transfer history for this player?", statusListed: "Listed", statusSold: "Sold", statusExpired: "Expired", statusBid: "Bid Placed", calcTitle: "Tax Reduction Calculator", lastTransferDate: "Last Transfer Date", daysElapsed: "Time Elapsed", currentTax: "Current Tax", days: "days", taxDrop50: "Drop to 50%", taxDrop15: "Drop to 15%", originalPlayer: "Original/Youth Player", originalNote: "Tax only depends on age.", taxDrop20: "Drop to 20% (Age 20)", taxDrop15_age: "Drop to 15% (Age 21)", // --- Section 7: Statistics Summary --- summarizeSeasons: "Summarize This Tab", summarizeAll: "SUMMARIZE ALL TABS", clearCache: "Reset", clearTrainingCache: "Clear All Training Cache", updateCurrentTab: "Update All Last Seasons", lastSeasonUpdateComplete: "All sections' last seasons updated!", summaryTitle: "Season Performance Summary", fetchingSeason: (season, current, total) => `Fetching season ${season}... (${current}/${total})`, chartTitle: (metric) => `${metric} History by Season`, statisticsTitle: "Season Performance Summary", statisticsDesc: "Presents league position and point changes from past seasons with charts.", // --- Section 10: Advanced Match Finder --- matchFinderTitle: "Advanced Match Finder", minRank: "Min Rank", maxRank: "Max Rank", minValue: "Min Value", maxValue: "Max Value", scanLeagues: "Scan Leagues", searchingTeams: (current, total) => `Searching Teams... (${current}/${total})`, matchFinderName: "Advanced Match Finder", resultsToolsTitle: "Advanced Match Finder", resultsToolsDesc: "Find past matches and tactics of opponent teams.", // --- Section 14: Friendly Match Automation --- friendlyMatchAutoTitle: "Auto Friendly Match Robot", friendlyMatchAutoDesc: "Sends auto friendly match offers with set tactics on scheduled days.", panelTitle: "MZ Auto Friendly Match Offer", autoModeLabel: "AUTO MODE (EVERY 6 HOURS)", settingsToggle: "🛠️ Tactic Settings", startBtn: "MANUAL START", stopBtn: "STOP", logOfferSent: (user, day) => `> ${user}: OFFER SENT FOR ${day}.`, dayNames: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"], modalHelper: "Only the 5 match days are listed:", btnSelectAll: "Select All", btnSave: "Save", btnCancel: "Cancel", settingsTitle: "Automation Settings", saveButton: "Save Settings", resetButton: "Reset Settings", closeButton: "Close", colOfferDay: "Offer Day", colMatchDay: "Match Day", colActive: "Active", colTacticHome: "Home Tactic", colTacticAway: "Away Tactic", inputPlaceholder: "Username...", addBtn: "Add", addFriendsBtn: "Fetch Friends", listHeader: "Will be sent to selected:", fmSelectAll: "All", fmDeleteSelected: "Delete", addedNewUsers: (count) => `${count} new users added.`, emptyList: "List empty. Add names or fetch friends.", alertSaved: "Settings Saved Permanently!", alertSaveError: "Error while saving: ", alertResetConfirm: "All settings will be deleted. Are you sure?", settingsNote: "Don't forget to click SAVE after making changes.", confirmDelete: (count) => `Are you sure you want to delete ${count} users permanently?`, noSelection: "No one selected!", noBuddyList: "Friend list is not open.", logSystemStart: (count) => `> SYSTEM STARTED. QUEUE: ${count} PEOPLE`, logRobotCall: `> BACKGROUND ROBOT IS BEING CALLED...`, logSearch: (user) => `> SEARCHING ${user}...`, logNotFound: (user) => `> ${user} NOT FOUND, SKIPPING.`, logNoDay: (user) => `> ${user}: NO SUITABLE DAY.`, logDayOff: "> DAILY SETTING IS OFF.", logErrorTimeout: '> ERROR: Page timeout.', logProcessDone: (user, remaining) => `> ${user} PROCESS DONE. REMAINING: ${remaining}`, logAllComplete: "> ALL TASKS COMPLETED. REPEATS IN 6 HOURS. ✅", // --- Section 16: Transfer Scout Filter --- tsfBtnTitle: "Scout Robot", tsfModalTitle: "Fast Transfer Scanner", tsfStartMsg: "Scans transfer list, analyzes HP (¹) and LP (²) stars of players.", tsfMinStarLabel: "Min High Potential (HP) to Save:", tsfStarInfo: "Only 2, 3 or 4 can be entered.", tsfBtnStart: "START SCAN", tsfBtnClose: "Close", tsfBtnStop: "STOP", tsfScanningMsg: "Fast Scanning & Analyzing... ", tsfWaitMsg: "Loading...", tsfPageInfo: "Processed Page: ", tsfFoundInfo: "Found Player: ", tsfFinishMsg: "Scan Complete!", tsfPanelTitle: "FILTER PANEL", tsfLblHP: "High Potential (HP)", tsfLblLP: "Low Potential (LP)", tsfLblSpeed: "Speed (AH)", tsfLblUnscouted: "Unscouted", tsfCountVisible: "Filtered Results: ", tsfPrevPage: "« Back", tsfNextPage: "Next »", tsfPageOf: "Page", tsfStoppedMsg: "Stopped.", tsfViewModeCard: "Card View", tsfViewModeTable: "Table View", tsfToggleFilters: "Detailed Potential Filters ▼", tsfFilterYP: "HP", tsfFilterNotYP: "No HP", tsfFilterNotDP: "No LP", tsfPresetLabel: "Saved Presets:", tsfBtnSavePreset: "Save", tsfBtnLoadPreset: "Load", tsfBtnDeletePreset: "Delete", tsfPhPresetName: "Preset Name...", tsfAlertPresetSaved: "Settings saved successfully!", tsfAlertPresetLoaded: "Settings loaded!", tsfAlertPresetDeleted: "Preset deleted.", tsfAlertEnterName: "Please enter a preset name.", transferScoutFilterTitle: "Scout Robot", transferScoutFilterDesc: "Filter transfer list by High/Low Potential stars.", // --- Other --- transferTrackerTitle: "Transfer History Tracker", transferTrackerDesc: "Tracks player transfer history and presents it with charts.", fixturePageEloTitle: "Fixture Page ELO Ratings", fixturePageEloDesc: "Displays ELO ratings of opponent teams on the fixture page.", maxedBallTitle: "Maxed Ball Coloring", maxedBallDesc: "Colors skills that have reached maximum level.", fixtureToolsTitle: "Analyze Opponents' Past Matches", fixtureToolsDesc: "View ELO scores for teams in the fixture list.", rankingTweaksTitle: "Manager Ranking Improvements", rankingTweaksDesc: "Quickly propose an observer match from the ranking page.", rankingScoutTitle: "Propose a scout match", ghostRobotTitle: "Set Piece Copier", ghostBtnTitle: "Copy Set Pieces", ghostRobotDesc: "Copies set piece organizations from one tactic to all others in seconds.", ghostLogReady: "[MZ GHOST] Ready.", ghostLogStart: "--- PROCESS STARTED ---", ghostErrorNoXML: "Source XML could not be retrieved.", ghostErrorNoPlayer: "No players found in source tactic.", ghostErrorNoTarget: "No other tactics found to copy to.", ghostConfirmMsg: (tabName, count) => `Set pieces from "${tabName}" will be copied to ${count} other tactics.\n\nPlease wait until the process finishes.\n\nStart?`, ghostSuccessMsg: "PROCESS SUCCESSFUL!\n\nPlease click the 'SAVE ALL' button below to finish.", ghostProcessing: "Processing...", linkEnhancementsTitle: "Link Enhancements", linkEnhancementsDesc: "Fixes team links on league pages and adds missing league links to youth matches.", messengerToolsTitle: "Messenger Bulk Delete", messengerToolsDesc: "Allows bulk deletion of conversations in the inbox.", messengerSelectAll: "Select All", messengerDeselectAll: "Deselect All", messengerDeleteSelected: "Delete Selected", messengerConfirmDelete: (count) => `${count} conversations will be permanently deleted. Are you sure?`, messengerDeleting: (current, total) => `Deleting: ${current}/${total}...`, messengerDeletionComplete: "Deletion complete.", messengerNoSelection: "Please select at least one conversation to delete.", copyHome: 'CopyXML (Home)', copyAway: 'CopyXML (Away)', copied: 'Tactic XML copied to clipboard!', copyError: 'Copy failed.', rBtnTitle: 'Show match result and tactics', errMsgNoData: 'Match data unavailable or could not be fetched after 5 tries.', matchDataFail: (status) => `Match data could not be prepared (status: ${status}).`, // --- Training Percentage --- trainingPercentageTitle: "Training Percentage Display", trainingPercentageDesc: "Automatically shows the progress percentage since the last ball in player profiles.", trainingAvg: "Average", trainingTotal: "Total", trainingBallDay: "Estimated days to next ball", trainingNow: "Current progress", trainingBall: "↑", residencyTitle: "Residency & Transfer Info", residencyDays: "Days at this club", transferFee: "Transfer Fee", entireCareer: "Entire Career", // --- General & Tactics --- scoutMatch: "Scout match", playMatchNow: "Play match now", load: "Load", copy: "Copy", paste: "Paste", rival: "Rival", location: "Location", roleGK: "GK", roleDef: "Defense", roleMid: "Midfield", roleFor: "Forward", tacticWings: "Wings", tacticShortPass: "Short pass", tacticLongBall: "Long ball", styleOffensive: "Offensive", styleNormal: "Normal", styleDefensive: "Defensive", aggrAggressive: "Aggressive", aggrNormal: "Normal", aggrPassive: "Passive", // --- Skills --- skillSpeed: "Speed", skillStamina: "Stamina", skillIntelligence: "Play Intelligence", skillPassing: "Passing", skillShooting: "Shooting", skillHeading: "Heading", skillKeeping: "Keeping", skillControl: "Ball Control", skillTackling: "Tackling", skillCrossing: "Crossing", skillSetPlays: "Set Plays", // --- Training/Scout Modal --- genelVerim: "General Efficiency", birimGun: "Units/D", updateTraining: "Update training data", rivalFormationTitle: "🕵️ Load Rival Formation", matchIdInstruction: "Enter the Match ID from the matches page", matchIdExample: "Ex: 1486585817", trainOrder: "Training order:", loserMsg: "Key skills too low — consider selling or dismissing.", sugTitle: "Training Suggestion", posCB: "CB", posGK: "GK", posWinger: "Winger", posStriker: "Striker", posMF: "MF", posPivot: "Pivot/CF", posDM: "DM", posWB: "WB", posAMF: "AMF", statMappings: { 'N': 'Rating', 'MP': 'Matches', 'G': 'Goals', 'A': 'Assists', 'S': 'Shots', 'G%': 'Goal %', 'SOT': 'SOT', 'SOT%': 'SOT %', 'P%': 'Pass %', 'PL': 'Key Pass', 'In': 'Interceptions', 'T%': 'Tackle %', 'Pos%': 'Poss.', 'Dist': 'Distance', 'DistP': 'Dist/Pass', 'SpP': 'Speed/Pass', 'SpR': 'Speed/Run', 'YC': 'YC', 'RC': 'RC', 'SV': 'Saves' }, rivalMatchTitle: "🕵️ Select Rival Team", matchId: "Match ID", rivalLoadError: "Could not fetch match data. The match might not have been processed yet.", rivalDataRequest: "⚠️ Data not found. Requesting server to prepare it...", totalValueLabel: "Total Value:", formationCopySuccess: "✅ Formation copied to clipboard!", formationPasteSuccess: "✅ Formation applied!", formationError10Players: "⚠️ Exactly 10 players must be on the pitch!", formationErrorClipboardEmpty: "⚠️ Clipboard is empty.", formationErrorInvalidFormat: "⚠️ Content must be 10 lines of coordinates or XML.", formationErrorNoCoords: "⚠️ 10 player positions not found.", formationErrorClipboardFail: "❌ Clipboard access failed.", formationErrorPasteFail: "❌ Could not read clipboard. Check browser permissions.", dragToMoveTooltip: "Hold to drag and move", tacticAutoFilled: "✅ Tactic auto-filled:", tacticEnterManually: "⚠️ Enter manually:" } }, getText: function (key, ...args) { const text = Core.i18n.TRANSLATIONS[Core.i18n.lang][key] || Core.i18n.TRANSLATIONS['en'][key]; if (!text) return `[${key}]`; if (typeof text === 'function') return text(...args); return text; }, getBundle: function (keys) { const bundle = {}; keys.forEach(key => bundle[key] = Core.i18n.getText(key)); return bundle; } }, utils: { formatMoney: (val) => (typeof val === 'number') ? val.toLocaleString('de-DE') + ' EUR' : val, wait: (ms) => new Promise(resolve => setTimeout(resolve, ms)), getGameTime: () => { if (typeof unsafeWindow.server_time === 'object' && unsafeWindow.server_time instanceof Date) { return unsafeWindow.server_time; } const timeStr = $('#header-stats-wrapper h5:first-child').text().match(/(\d{2}-\d{2}-\d{4}\s\d{2}:\d{2})/); if (timeStr) { const parts = timeStr[1].match(/(\d{2})-(\d{2})-(\d{4})\s(\d{2}):(\d{2})/); return new Date(parts[3], parts[2] - 1, parts[1], parts[4], parts[5]); } return new Date(); } } }; const getText = Core.i18n.getText; const t = getText; // try { /**************************************************************************************** * * * BÖLÜM 1: GENEL GÖZLEMCİ MAÇI PENCERESİ DÜZENLEMESİ (v8 - Hata Ayıklama Modlu) * * * ****************************************************************************************/ // Replaced setInterval with MutationObserver for better performance function initializeGlobalScoutMatchListener() { document.body.addEventListener('mousedown', async function (event) { // 1. ADIM: İlgili olabilecek bir butona tıklandığını tespit et const scoutButton = event.target.closest('a[onclick*="instantchallenge"], a[onclick*="purchaseChallenge"]'); if (!scoutButton) return; const onclickAttr = scoutButton.getAttribute('onclick'); let teamId = null; let teamIdMatch; // 2. ADIM: Takım ID'sini bulmak için iki farklı strateji dene // Strateji A: Standart "Gözlemci Maçı" butonu (instantchallenge) teamIdMatch = onclickAttr.match(/instantchallenge.+?['"](\d+)['"]/); if (teamIdMatch && teamIdMatch[1]) { teamId = teamIdMatch[1]; } else { // Strateji B: Federasyon sayfasındaki "Meydan Oku" butonu (purchaseChallenge) teamIdMatch = onclickAttr.match(/purchaseChallenge\(.*,\s*['"]?(\d+)['"]?\)/); if (teamIdMatch && teamIdMatch[1]) { teamId = teamIdMatch[1]; } } if (!teamId) return; // 3. ADIM: Takım adını bul let teamName = `(ID: ${teamId})`; try { // Butonun bulunduğu en yakın satır (`tr`) veya `div`'i bul. const container = scoutButton.closest('tr, div.flex-nowrap'); let teamLink = null; if (container) { // Önce, Federasyon sayfasındaki gibi `.team-name` sınıfına sahip linki ara. Bu daha spesifik. teamLink = container.querySelector('a.team-name'); if (!teamLink) { // Eğer bulunamazsa, standart `tid=` içeren linki ara. teamLink = container.querySelector(`a[href*="tid=${teamId}"]`); } } if (teamLink) { teamName = teamLink.textContent.trim().replace(/\s\s+/g, ' '); } else { // Yedek olarak takım sayfasından adı çekme. const response = await new Promise((resolve, reject) => GM_xmlhttpRequest({ method: "GET", url: `/?p=team&tid=${teamId}`, onload: resolve, onerror: reject })); const doc = new DOMParser().parseFromString(response.responseText, 'text/html'); const titleElement = doc.querySelector('h1.win_title'); if (titleElement) { teamName = titleElement.textContent.trim(); } } } catch (error) { console.error('Takım adı aranırken bir hata oluştu:', error); } // 4. ADIM: Açılan pencereyi izle ve takım adını ekle (MutationObserver ile optimize edildi) const observer = new MutationObserver((mutations, obs) => { const powerboxTitleDiv = document.querySelector('div.mz_powerboxTitle'); if (powerboxTitleDiv && powerboxTitleDiv.offsetParent !== null) { const nobrTag = powerboxTitleDiv.querySelector('nobr'); if (nobrTag) { const titleText = nobrTag.textContent; const isScoutModal = titleText.includes(Core.i18n.TRANSLATIONS['tr'].scoutMatch) || titleText.includes(Core.i18n.TRANSLATIONS['en'].scoutMatch) || titleText.includes(Core.i18n.TRANSLATIONS['tr'].playMatchNow) || titleText.includes(Core.i18n.TRANSLATIONS['en'].playMatchNow); if (isScoutModal) { let nameSpan = nobrTag.querySelector('.injected-team-name'); if (!nameSpan) { nameSpan = document.createElement('span'); nameSpan.className = 'injected-team-name'; nameSpan.style.fontWeight = 'bold'; nobrTag.appendChild(nameSpan); } nameSpan.textContent = ` - ${teamName}`; obs.disconnect(); // İşimiz bitti, dinlemeyi bırak clearTimeout(timeoutId); // Zaman aşımını temizle } } } }); observer.observe(document.body, { childList: true, subtree: true }); // 5 Saniye zaman aşımı (Pencere açılmazsa observer'ı kapat) const timeoutId = setTimeout(() => { observer.disconnect(); }, 5000); }, true); } // ▼▼▼ AYAR ALTYAPISI BAŞLANGICI ▼▼▼ const MODULES = { leagueTable: { nameKey: "leagueTitle" }, playerStats: { nameKey: "playersTitle" }, playoffPredictor: { nameKey: "fixtureToolsTitle" }, shortlistFilter: { nameKey: "shortlistTitle" }, transferTracker: { nameKey: "transferTrackerTitle" }, statisticsSummary: { nameKey: "statisticsTitle" }, retirementIndicator: { nameKey: "retireWarning" }, matchFinder: { nameKey: "matchFinderName" }, messengerTools: { nameKey: "messengerToolsTitle" }, linkEnhancements: { nameKey: "linkEnhancementsTitle" }, transferScoutFilter: { nameKey: "transferScoutFilterTitle" }, residencyInfo: { nameKey: "residencyTitle" } }; const MODULE_SETTINGS_KEY = 'mz_advanced_script_module_settings_v1'; let scriptSettings = {}; async function loadScriptSettings() { const defaultSettings = {}; for (const key in MODULES) { defaultSettings[key] = true; } const savedSettings = JSON.parse(await GM_getValue(MODULE_SETTINGS_KEY, '{}')); scriptSettings = { ...defaultSettings, ...savedSettings }; Object.assign(scriptSettings, { ghostRobot: false, friendlyMatchAuto: false, skillColoring: false, maxedBallColoring: false, trainingPercentage: false, fixtureElo: false }); Core.settings = scriptSettings; } // ▲▲▲ AYAR ALTYAPISI SONU ▲▲▲ /**************************************************************************************** * * * BÖLÜM 2: GELİŞMİŞ LİG TABLOSU BETİĞİ (ManagerZone Universal Advanced League Table) * * * ****************************************************************************************/ function initializeLeagueTableScript() { // ================================================================================= // BÖLÜM 2.1: ULUSLARARASILAŞTIRMA (i18n) // ================================================================================= const i18n = { translations: { en: { scriptName: 'ManagerZone Universal Advanced League Table', leagueTableTitle: 'Table', fixtureDifficultyHeader: 'FD', fixtureDifficultyTitle: 'Fixture Difficulty (Higher value is advantageous, lower is disadvantageous)', settingsTitle: 'Change settings', fetchLiveScoresBtn: 'Fetch Live Scores', processingBtn: 'Processing...', tableUpdatedBtn: 'Table Updated!', scriptUpdatedTitle: "'${scriptName}' has been updated!", updateNotesIntro: "Here's what's new in version v${version}:", updateModalCloseBtn: 'Got it, Close', settingsModalTitle: 'Settings', logoVisibilityLabel: 'Logo Visibility (Show team logos)', saveBtn: 'Save', roundPrefix: 'Round', fzNotAvailable: 'Fixture analysis is not available for this page.', fzTooltip: 'Remaining matches: ${remaining}\nOpponent rank sum: ${rankSum}', fzNoMatchesLeft: 'No matches left', locationHome: '(H)', locationAway: '(A)', homeAwayFiltersLabel: 'Table View Filters (Overall/Home/Away)', /* YENİ EKLENEN ÇEVİRİLER */ home: 'Home', away: 'Away', overall: 'Overall', updateNotes: { '2.6': ["Fixed a critical bug where the script would fail on non-Turkish/English languages. The script now correctly parses fixture data regardless of the selected language and uses an English UI for all non-Turkish languages."] } }, tr: { scriptName: 'ManagerZone Evrensel Gelişmiş Lig Tablosu', leagueTableTitle: 'Puan Tablosu', fixtureDifficultyHeader: 'FZ', fixtureDifficultyTitle: 'Fikstür Zorluğu (Değer yüksekse avantajlı, düşükse dezavantajlı)', settingsTitle: 'Ayarları değiştir', fetchLiveScoresBtn: 'Canlı Maç Sonuçlarını Al', processingBtn: 'İşleniyor...', tableUpdatedBtn: 'Tablo Güncellendi!', scriptUpdatedTitle: "'${scriptName}' Güncellendi!", updateNotesIntro: "Yeni sürüm (v${version}) ile gelen yenilikler:", updateModalCloseBtn: 'Anladım, Kapat', settingsModalTitle: 'Ayarlar', logoVisibilityLabel: 'Logo Görünürlüğü (Takım logoları gösterilsin)', saveBtn: 'Kaydet', roundPrefix: 'Tur', fzNotAvailable: 'Fikstür analizi bu sayfa için mevcut değil.', fzTooltip: 'Kalan maç: ${remaining}\nRakip sıra top: ${rankSum}', fzNoMatchesLeft: 'Kalan maç yok', locationHome: '(E)', locationAway: '(D)', homeAwayFiltersLabel: 'Puan Tablosu Filtresi (Genel/İç Saha/Dış Saha)', /* YENİ EKLENEN ÇEVİRİLER */ home: 'İç Saha', away: 'Dış Saha', overall: 'Genel', updateNotes: { '2.6': ["Türkçe/İngilizce dışındaki dillerde betiğin çalışmamasına neden olan kritik hata düzeltildi. Betik artık seçilen dilden bağımsız olarak fikstür verisini doğru bir şekilde analiz ediyor ve Türkçe dışındaki tüm diller için İngilizce arayüz kullanıyor."] } } }, detectLanguage: function () { const langMeta = document.querySelector('meta[name="language"]'); if (langMeta && langMeta.getAttribute('content') === 'tr') { return 'tr'; } return 'en'; }, get: function (key, replacements = {}) { let lang = this.currentLang || this.detectLanguage(); let text; if (this.translations[lang] && this.translations[lang][key] !== undefined) { text = this.translations[lang][key]; } else { text = this.translations['en']?.[key]; } if (text === undefined) { console.warn(`i18n key not found: ${key}`); return `[${key}]`; } if (key === 'updateNotes' && typeof text === 'object') { return text; } for (const placeholder in replacements) { text = text.replace(new RegExp(`\\$\\{${placeholder}\\}`, 'g'), replacements[placeholder]); } return text; } }; i18n.currentLang = i18n.detectLanguage(); // ================================================================================= // BÖLÜM 2.2: YAPILANDIRMA VE SABİTLER // ================================================================================= const CONFIG = { SELECTORS: { LEAGUE_TABLE: 'table.nice_table', TABLE_HEADER_ROW: 'table.nice_table thead tr', TABLE_BODY: 'table.nice_table tbody', TABLE_ROWS: 'table.nice_table tbody tr', TEAM_LINK_IN_ROW: 'a[href*="&tid="]', MY_TEAM_ROW: 'tr.highlight_row', ONGOING_MATCH_LINK: 'table.hitlist a[href*="mid="], .team-matches-vs a[href*="mid="]', LEAGUE_TABLE_HEADER: 'h2.subheader', SCHEDULE_TAB_LINK: '#league_tab_schedule', UPDATE_BUTTON: '.mz-fetch-button', SETTINGS_BUTTON: '.mz-settings-button', CONTENT_DIV: '#contentDiv', SETTINGS_MODAL: '#mz-settings-modal', SETTINGS_MODAL_CLOSE: '.mz-modal-close', SETTINGS_MODAL_SAVE: '#mz-settings-save', LOGO_VISIBILITY_CHECKBOX: '#mz-logo-visibility-checkbox', HOME_AWAY_FILTERS_CHECKBOX: '#mz-home-away-filters-checkbox', UPDATE_MODAL: '#mz-update-modal', UPDATE_MODAL_CLOSE: '#mz-update-close-button', }, CLASSES: { NEXT_OPPONENT: 'mz-next-opponent', UPDATED_CELL: 'updated-cell', INDICATORS_WRAPPER: 'mz-indicators-wrapper', TOOLTIP: 'mz-custom-tooltip', BUTTON_DISABLED: 'disabled', BUTTON_DONE: 'done', FZ_HEADER: 'mz-fz-header', TEAM_LOGO: 'mz-team-logo', SETTINGS_ACTIVE: 'mz-settings-active', SETTINGS_BUTTON: 'mz-settings-button', // <-- BU SATIRI EKLEYİN }, COLUMNS: { POSITION: 0, TEAM_NAME: 1, PLAYED: 2, WINS: 3, DRAWS: 4, LOSSES: 5, GOALS_FOR: 6, GOALS_AGAINST: 7, GOAL_DIFFERENCE: 8, POINTS: 9, FIXTURE_DIFFICULTY: 10 }, API: { MATCH_INFO: 'https://www.managerzone.com/xml/match_info.php?sport_id=1&match_id=', } }; const SCRIPT_INFO = GM_info.script; const LAST_SEEN_VERSION_KEY = 'mz_advanced_table_last_version'; const UPDATE_NOTES = i18n.get('updateNotes'); function injectStyles() { GM_addStyle(GM_getResourceText('NPROGRESS_CSS')); GM_addStyle(` .mz-team-cell { display: flex; align-items: center; } .mz-team-logo { height: 2.0em; width: auto; margin-right: 6px; vertical-align: middle; flex-shrink: 0; pointer-events: none; } .mz-indicators-wrapper { margin-left: auto; margin-right: 15px; display: flex; align-items: center; white-space: nowrap; padding-left: 10px; pointer-events: none; } .match-indicator { display: inline-block; width: 13.5px; height: 12.5px; border-radius: 50%; margin-left: 8px; vertical-align: middle; border: 1px solid rgba(0,0,0,0.6); cursor: pointer; flex-shrink: 0; pointer-events: auto; } .match-win { background-color: #28a745; } .match-loss { background-color: #dc3545; } .match-draw { background-color: #ffc107; } .match-future { font-size: 11px; color: #000; margin-left: 5px; font-style: italic; font-weight: bold; } .mz-next-opponent { background-color: #fff3cd !important; font-weight: bold; } .mz-next-opponent:hover { background-color: #ffeeba !important; } .mz-next-opponent a { color: #856404 !important; } .mz-custom-tooltip { position: absolute; display: none; padding: 8px 12px; color: #fff; border-radius: 6px; font-size: 14px; font-weight: bold; z-index: 9999; pointer-events: none; box-shadow: 0 4px 8px rgba(0,0,0,0.3); text-shadow: 1px 1px 2px rgba(0,0,0,0.5); line-height: 1; white-space: nowrap; } .mz-custom-tooltip::before { content: ''; position: absolute; top: 50%; right: 100%; margin-top: -6px; border-width: 6px; border-style: solid; } .tooltip-win { background-color: #28a745; border: 1px solid #1e7e34; } .tooltip-win::before { border-color: transparent #1e7e34 transparent transparent; } .tooltip-loss { background-color: #dc3545; border: 1px solid #b21f2d; } .tooltip-loss::before { border-color: transparent #b21f2d transparent transparent; } .tooltip-draw { background-color: #ffc107; border: 1px solid #d39e00; color: #212529; text-shadow: none; } .tooltip-draw::before { border-color: transparent #d39e00 transparent transparent; } .mz-fetch-button { margin-left: 10px; padding: 5px 10px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; transition: background-color 0.3s; } .mz-fetch-button:hover:not(.disabled) { background-color: #45a049; } .mz-fetch-button.disabled { background-color: #ccc; cursor: not-allowed; } .mz-fetch-button.done { background-color: #007bff; } .${CONFIG.CLASSES.FZ_HEADER}, table.nice_table td:nth-child(${CONFIG.COLUMNS.FIXTURE_DIFFICULTY + 1}) { text-align: center; font-weight: bold; cursor: help; } .mz-settings-button { margin-left: 8px; cursor: pointer; color: #999; font-size: 1.6em; transition: color 0.3s; vertical-align: middle;} .mz-settings-button:hover { color: #333; } .mz-settings-button.mz-settings-active { color: #4CAF50; } .mz-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); z-index: 10000; display: none; justify-content: center; align-items: center; } .mz-modal-content { background: #fefefe; padding: 20px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); min-width: 300px; display: flex; flex-direction: column; max-width: 500px; } .mz-modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #ddd; padding-bottom: 10px; margin-bottom: 15px; } .mz-modal-header h3 { margin: 0; font-size: 18px; } .mz-modal-close { font-size: 24px; font-weight: bold; cursor: pointer; color: #888; } .mz-modal-close:hover { color: #000; } .mz-modal-body { display: flex; flex-direction: column; gap: 15px; } .mz-modal-body label { display: flex; align-items: center; font-size: 16px; cursor: pointer; } .mz-modal-body input[type="checkbox"] { width: 18px; height: 18px; margin-right: 10px; } .mz-modal-footer { border-top: 1px solid #ddd; padding-top: 15px; margin-top: 20px; text-align: right; } #mz-settings-save { padding: 8px 16px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; } #mz-settings-save:hover { background-color: #45a049; } #mz-update-modal .mz-modal-body ul { list-style-type: none; padding-left: 0; margin: 10px 0; } #mz-update-modal .mz-modal-body li { margin-bottom: 10px; padding-left: 20px; position: relative; } #mz-update-modal .mz-modal-body li::before { content: '✓'; color: #4CAF50; position: absolute; left: 0; font-weight: bold; } #mz-update-modal .mz-modal-footer { text-align: center; } #mz-update-close-button { padding: 10px 25px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; } #mz-update-close-button:hover { background-color: #0069d9; } .match-indicator, .match-future, .mz-indicators-wrapper { z-index: 5 !important; position: relative; } /* Sitenin mobil üst menüsü ve Messenger yan panelinin her zaman üstte kalmasını sağlayalım */ @media (max-width: 900px) { #header, #top-bar, .header-top, .main-lateral-panel-wrapper, #lateral-panel, #ajax-lateral-panel { z-index: 999999 !important; pointer-events: auto !important; } } @media (max-width: 1100px) { .mz-team-logo { height: 1.5em !important; width: auto !important; max-width: 22px !important; pointer-events: none !important; } /* === MOBİL GÖRÜNÜM DÜZELTMESİ === */ .mz-indicators-wrapper { flex-wrap: nowrap; margin-left: auto; margin-right: 5px; justify-content: flex-end; pointer-events: none !important; /* Tıklamalar alttaki takım linkine geçsin */ } .match-indicator { width: 11px !important; height: 10px !important; margin-left: 5px !important; pointer-events: none !important; } .match-future { font-size: 10px !important; margin-left: 3px !important; pointer-events: none !important; } /* Telefon yan çevrilince (Landscape) isteğe bağlı olarak eklentileri gizle veya daha da küçült */ /* Kullanıcının asıl isteği: Düzen dağılıyorsa çalışmasın (gizlensin) */ @media (orientation: landscape) { /* Mobilde yan çevrilince temiz görünüm için boyutları optimize et */ .mz-team-logo { height: 1.2em !important; } .match-indicator { width: 9px !important; height: 8px !important; } .mz-team-cell { display: flex !important; align-items: center; } } @media (max-width: 480px) { .mz-team-cell { white-space: nowrap !important; word-break: normal !important; overflow: hidden !important; min-width: 0 !important; } .mz-team-cell a { min-width: 0 !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; flex-shrink: 1 !important; flex-grow: 0 !important; display: block !important; max-width: 100% !important; } .mz-team-logo { margin-right: 4px !important; flex-shrink: 0 !important; } .mz-indicators-wrapper { flex-shrink: 0 !important; } } /* === TAKTİK SAYFASI MOBİL DÜZENLEMELERİ === */ #xml_ghost_btn { position: relative !important; top: 0 !important; right: 0 !important; margin: 8px 0 !important; display: block !important; width: 100% !important; box-sizing: border-box !important; text-align: center !important; } #mz-coord-panel { display: flex !important; flex-wrap: wrap !important; width: 100% !important; margin-left: 0 !important; margin-top: 12px !important; gap: 6px !important; justify-content: flex-start !important; } #mz-coord-panel > button { margin-left: 0 !important; padding: 8px 12px !important; flex: 1 1 auto !important; min-width: 80px !important; } #mz-history-btn { order: 10 !important; /* En sona koy */ } /* Training Percentage Styles */ .training-percent { display: inline-block; margin-left: 8px; padding: 1px 4px; background-color: #f1f8e9; border: 1px solid #8bc34a; border-radius: 4px; font-size: 10px; font-weight: bold; color: #33691e; cursor: help; vertical-align: middle; } .training-tooltip { background: #263238 !important; color: #fff !important; padding: 8px !important; border-radius: 4px !important; font-size: 11px !important; line-height: 1.4 !important; box-shadow: 0 2px 5px rgba(0,0,0,0.5) !important; z-index: 10001 !important; } .training-ball-icon { color: #4caf50; margin-left: 2px; font-weight: bold; } .mz-team-cell a { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } } /* YENİ EKLENEN ELO STİLİ */ .elo-score-header, .elo-score-cell { text-align: center !important; font-weight: bold; cursor: help; } .mz-table-filters .mzbtn span { text-shadow: none !important; } /* FIKSTÜR SAYFASI ELO PUANI STILI */ .fixture-elo-score { font-size: 11px; font-weight: bold; color: #555; background-color: #f0f0f0; padding: 1px 4px; border-radius: 3px; border: 1px solid #ddd; margin-top: 3px; display: inline-block; } /* === MOBIL MASTER LAUNCHER (ANA TETIKLEYICI) === */ #mz-mobile-master-launcher { position: fixed; left: 0; top: 40%; width: 30px; height: 35px; background: #00529B; color: #fff; border: 1px solid #fff; border-left: none; border-radius: 0 8px 8px 0; display: none; align-items: center; justify-content: center; font-size: 11px; font-weight: bold; z-index: 1000001; box-shadow: 2px 0 5px rgba(0,0,0,0.3); cursor: pointer; } @media (max-width: 900px) { #mz-mobile-master-launcher { display: flex; } /* Başlangıçta gizli durumlar */ .mz-mobile-stashed { display: none !important; } } `); } function request(options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...options, onload: resolve, onerror: reject, ontimeout: reject }); }); } let myTeamInfo = { id: null, name: null }; let originalTableBody = null; const liveMatchResults = new Map(); const tooltip = document.createElement('div'); document.body.appendChild(tooltip); let fullScheduleCache = null; let isLogoDisplayEnabled = true; let isHomeAwayFiltersEnabled = true; let allPlayedMatchesCache = null; // YENİ EKLENEN DEĞİŞKEN let isAddingEloScores = false; function findOngoingMatches() { const ongoingMatches = []; document.querySelectorAll(CONFIG.SELECTORS.ONGOING_MATCH_LINK).forEach(link => { const scoreText = link.textContent.trim(); if (!/^\d+\s*-\s*\d+$/.test(scoreText) && !scoreText.includes('X - X')) { const mid = new URLSearchParams(link.href).get('mid'); if (mid) ongoingMatches.push({ mid, linkElement: link }); } }); return ongoingMatches; } function findMainLeagueTableHeader() { const allHeaders = document.querySelectorAll(CONFIG.SELECTORS.LEAGUE_TABLE_HEADER); for (const header of allHeaders) { const nextElement = header.nextElementSibling; if (nextElement && (nextElement.matches(CONFIG.SELECTORS.LEAGUE_TABLE) || nextElement.querySelector(CONFIG.SELECTORS.LEAGUE_TABLE))) { return header; } } return null; } function isMainLeagueTableVisible() { // Mobilde tablo arkada kalsa bile DOM'da duruyor olabilir, bu yüzden fiziksel görünürlük de bakmalıyız const tables = document.querySelectorAll('#contentDiv table.nice_table'); for (const table of tables) { const header = table.querySelector('thead'); const body = table.querySelector('tbody'); if (header && body && body.rows.length > 2 && header.rows[0].cells.length >= 8) { // offsetParent null ise element gizlidir (arka plandadır veya display:none) return mzIsMobile ? (table.offsetParent !== null) : true; } } return false; } function setupLiveUpdateFeature() { const ongoingMatches = findOngoingMatches(); if (ongoingMatches.length === 0) return; const titleHeader = findMainLeagueTableHeader(); if (!titleHeader || document.querySelector(CONFIG.SELECTORS.UPDATE_BUTTON)) return; const button = document.createElement('button'); button.className = 'mz-fetch-button'; button.textContent = i18n.get('fetchLiveScoresBtn'); button.addEventListener('click', () => handleLiveUpdateClick(button, ongoingMatches)); titleHeader.appendChild(button); } function createUpdateModal(version, notes) { if (document.querySelector(CONFIG.SELECTORS.UPDATE_MODAL)) return; const modalOverlay = document.createElement('div'); modalOverlay.id = 'mz-update-modal'; modalOverlay.className = 'mz-modal-overlay'; const notesHtml = notes.map(note => `<li>${note}</li>`).join(''); const scriptName = i18n.get('scriptName'); modalOverlay.innerHTML = ` <div class="mz-modal-content"> <div class="mz-modal-header"> <h3>${i18n.get('scriptUpdatedTitle', { scriptName })}</h3> </div> <div class="mz-modal-body"> <p>${i18n.get('updateNotesIntro', { version })}</p> <ul>${notesHtml}</ul> </div> <div class="mz-modal-footer"> <button id="mz-update-close-button">${i18n.get('updateModalCloseBtn')}</button> </div> </div>`; document.body.appendChild(modalOverlay); modalOverlay.querySelector(CONFIG.SELECTORS.UPDATE_MODAL_CLOSE).addEventListener('click', async () => { modalOverlay.style.display = 'none'; await GM_setValue(LAST_SEEN_VERSION_KEY, SCRIPT_INFO.version); }); } function createSettingsModal() { if (document.querySelector(CONFIG.SELECTORS.SETTINGS_MODAL)) return; const modalOverlay = document.createElement('div'); modalOverlay.id = 'mz-settings-modal'; modalOverlay.className = 'mz-modal-overlay'; modalOverlay.innerHTML = ` <div class="mz-modal-content"> <div class="mz-modal-header"> <h3>${i18n.get('settingsModalTitle')}</h3> <span class="mz-modal-close">×</span> </div> <div class="mz-modal-body"> <label> <input type="checkbox" id="mz-logo-visibility-checkbox" /> ${i18n.get('logoVisibilityLabel')} </label> <label> <input type="checkbox" id="mz-home-away-filters-checkbox" /> ${i18n.get('homeAwayFiltersLabel')} </label> </div> <div class="mz-modal-footer"> <button id="mz-settings-save">${i18n.get('saveBtn')}</button> </div> </div>`; document.body.appendChild(modalOverlay); modalOverlay.querySelector(CONFIG.SELECTORS.SETTINGS_MODAL_CLOSE).addEventListener('click', closeSettingsModal); modalOverlay.querySelector(CONFIG.SELECTORS.SETTINGS_MODAL_SAVE).addEventListener('click', saveSettingsAndClose); modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) { closeSettingsModal(); } }); } function openSettingsModal() { const modal = document.querySelector(CONFIG.SELECTORS.SETTINGS_MODAL); const logoCheckbox = document.querySelector(CONFIG.SELECTORS.LOGO_VISIBILITY_CHECKBOX); const filtersCheckbox = document.querySelector(CONFIG.SELECTORS.HOME_AWAY_FILTERS_CHECKBOX); if (!modal || !logoCheckbox || !filtersCheckbox) return; logoCheckbox.checked = isLogoDisplayEnabled; filtersCheckbox.checked = isHomeAwayFiltersEnabled; modal.style.display = 'flex'; } function closeSettingsModal() { const modal = document.querySelector(CONFIG.SELECTORS.SETTINGS_MODAL); if (modal) modal.style.display = 'none'; } function saveSettingsAndClose() { const logoCheckbox = document.querySelector(CONFIG.SELECTORS.LOGO_VISIBILITY_CHECKBOX); const filtersCheckbox = document.querySelector(CONFIG.SELECTORS.HOME_AWAY_FILTERS_CHECKBOX); if (logoCheckbox) { isLogoDisplayEnabled = logoCheckbox.checked; GM_setValue('showTeamLogos', isLogoDisplayEnabled); updateSettingsIconState(); toggleLogoVisibility(); } if (filtersCheckbox) { isHomeAwayFiltersEnabled = filtersCheckbox.checked; GM_setValue('showHomeAwayFilters', isHomeAwayFiltersEnabled); toggleHomeAwayFiltersVisibility(); } closeSettingsModal(); } function setupSettingsMenu() { const titleHeader = findMainLeagueTableHeader(); if (!titleHeader || document.querySelector(CONFIG.SELECTORS.SETTINGS_BUTTON)) return; const settingsIcon = document.createElement('span'); settingsIcon.className = CONFIG.CLASSES.SETTINGS_BUTTON; settingsIcon.textContent = '⚙️'; settingsIcon.addEventListener('click', openSettingsModal); titleHeader.appendChild(settingsIcon); updateSettingsIconState(); } function updateSettingsIconState() { const settingsIcon = document.querySelector(CONFIG.SELECTORS.SETTINGS_BUTTON); if (!settingsIcon) return; settingsIcon.classList.toggle(CONFIG.CLASSES.SETTINGS_ACTIVE, isLogoDisplayEnabled); settingsIcon.title = i18n.get('settingsTitle'); } function toggleHomeAwayFiltersVisibility() { // İlk olarak, filtre butonlarının şu an sayfada olup olmadığını kontrol et const filters = document.querySelector('.mz-table-filters'); // Eğer ayarlarda "Filtre Butonları" seçeneği AÇIK ise: if (isHomeAwayFiltersEnabled) { // ve butonlar henüz ekranda YOKSA: if (!filters) { // Butonları oluşturan fonksiyonu çağırarak onları ekrana getir. setupHomeAwayFilters(); } } // Eğer ayarlarda "Filtre Butonları" seçeneği KAPALI ise: else { // ve butonlar şu an ekranda VARSA: if (filters) { // Onları ekrandan tamamen kaldır. filters.remove(); } } } async function handleLiveUpdateClick(button, matches) { button.textContent = i18n.get('processingBtn'); button.classList.add(CONFIG.CLASSES.BUTTON_DISABLED); button.disabled = true; NProgress.start(); const currentTbody = document.querySelector(CONFIG.SELECTORS.TABLE_BODY); if (currentTbody && originalTableBody) { currentTbody.parentNode.replaceChild(originalTableBody.cloneNode(true), currentTbody); addFixtureDifficultyColumn(); toggleLogoVisibility(); } liveMatchResults.clear(); for (let i = 0; i < matches.length; i++) { NProgress.set((i / matches.length) * 0.5); try { const response = await request({ method: 'GET', url: CONFIG.API.MATCH_INFO + matches[i].mid }); const xmlDoc = new DOMParser().parseFromString(response.responseText, 'application/xml'); const homeNode = xmlDoc.querySelector('Team[field="home"]'); const awayNode = xmlDoc.querySelector('Team[field="away"]'); if (homeNode && awayNode) { const matchData = { mid: matches[i].mid, homeTid: homeNode.getAttribute('id'), awayTid: awayNode.getAttribute('id'), homeGoals: parseInt(homeNode.getAttribute('goals'), 10) || 0, awayGoals: parseInt(awayNode.getAttribute('goals'), 10) || 0 }; liveMatchResults.set(matchData.mid, matchData); matches[i].linkElement.textContent = `${matchData.homeGoals} - ${matchData.awayGoals}`; } } catch (error) { console.error(`Maç ID ${matches[i].mid} için veri çekilemedi:`, error); } } NProgress.set(0.6); liveMatchResults.forEach(data => { const { homeResult, awayResult } = calculateMatchResult(data); updateTeamRow(data.homeTid, homeResult); updateTeamRow(data.awayTid, awayResult); }); NProgress.set(0.8); sortTableByPoints(); await calculateAndDisplayFixtureDifficulty(liveMatchResults); if (myTeamInfo.id) { NProgress.set(0.9); await performFullOpponentAnalysis(); } NProgress.done(); button.textContent = i18n.get('tableUpdatedBtn'); button.classList.add(CONFIG.CLASSES.BUTTON_DONE); } function calculateMatchResult(data) { if (data.homeGoals > data.awayGoals) return { homeResult: { p: 3, w: 1, d: 0, l: 0, gf: data.homeGoals, ga: data.awayGoals }, awayResult: { p: 0, w: 0, d: 0, l: 1, gf: data.awayGoals, ga: data.homeGoals } }; if (data.homeGoals < data.awayGoals) return { homeResult: { p: 0, w: 0, d: 0, l: 1, gf: data.homeGoals, ga: data.awayGoals }, awayResult: { p: 3, w: 1, d: 0, l: 0, gf: data.awayGoals, ga: data.homeGoals } }; return { homeResult: { p: 1, w: 0, d: 1, l: 0, gf: data.homeGoals, ga: data.awayGoals }, awayResult: { p: 1, w: 0, d: 1, l: 0, gf: data.awayGoals, ga: data.homeGoals } }; } function updateTeamRow(tid, result) { const teamRow = document.querySelector(`.nice_table a[href*="&tid=${tid}"]`)?.closest('tr'); if (!teamRow) return; const cells = teamRow.cells; const C = CONFIG.COLUMNS; const parseCellInt = (index) => parseInt(cells[index]?.textContent.trim(), 10) || 0; const updateCell = (index, value) => { if (cells[index]) { cells[index].textContent = value; cells[index].classList.add(CONFIG.CLASSES.UPDATED_CELL); setTimeout(() => cells[index].classList.remove(CONFIG.CLASSES.UPDATED_CELL), 2000); } }; updateCell(C.PLAYED, parseCellInt(C.PLAYED) + 1); updateCell(C.WINS, parseCellInt(C.WINS) + result.w); updateCell(C.DRAWS, parseCellInt(C.DRAWS) + result.d); updateCell(C.LOSSES, parseCellInt(C.LOSSES) + result.l); const newGF = parseCellInt(C.GOALS_FOR) + result.gf; const newGA = parseCellInt(C.GOALS_AGAINST) + result.ga; updateCell(C.GOALS_FOR, newGF); updateCell(C.GOALS_AGAINST, newGA); updateCell(C.GOAL_DIFFERENCE, newGF - newGA); updateCell(C.POINTS, parseCellInt(C.POINTS) + result.p); } function applyPromotionRelegationStyles() { const tableRows = document.querySelectorAll(CONFIG.SELECTORS.TABLE_ROWS); if (!tableRows || tableRows.length < 1) return; // Önce mevcut tüm stilleri temizle ki yanlış yerde kalmasınlar. tableRows.forEach(row => { row.style.borderBottom = ''; }); // GÜVENİLİR YÖNTEM: Lig türünü sayfa URL'sinden al const urlParams = new URLSearchParams(window.location.search); const leagueType = urlParams.get('type') || ''; // Örn: 'u23_world', 'senior', 'world_series' // Lig türü URL'de 'world' kelimesini içeriyorsa, bu bir Dünya Ligi'dir. if (leagueType.includes('world')) { // === DÜNYA LİGİ KURALLARI (U18, U21, U23 dahil) === // 1. sıraya düz yeşil çizgi (Şampiyon) if (tableRows.length >= 1) { const promotionRow = tableRows[0]; // 1. sıra (index 0) promotionRow.style.borderBottom = '2px solid green'; } // 2. sıraya kesikli yeşil çizgi (Play-Off) if (tableRows.length >= 2) { const playOffRow = tableRows[1]; // 2. sıra (index 1) playOffRow.style.borderBottom = '2px dashed green'; } // 8. sıraya düz kırmızı çizgi (Düşme) if (tableRows.length >= 8) { const relegationRow = tableRows[7]; // 8. sıra (index 7) relegationRow.style.borderBottom = '2px solid red'; } } else { // === NORMAL LİG KURALLARI === // 7. sıraya kesikli kırmızı çizgi (Play-out) if (tableRows.length >= 7) { const playOutRow = tableRows[6]; // 7. sıra (index 6) playOutRow.style.borderBottom = '2px dashed #D60000'; } // 9. sıraya düz kırmızı çizgi (Düşme) if (tableRows.length >= 9) { const relegationRow = tableRows[8]; // 9. sıra (index 8) relegationRow.style.borderBottom = '2px solid red'; } } } function sortTableByPoints() { const tbody = document.querySelector(CONFIG.SELECTORS.TABLE_BODY); if (!tbody) return; const rows = Array.from(tbody.rows); const C = CONFIG.COLUMNS; rows.sort((a, b) => { const val = (r, i) => parseInt(r.cells[i]?.textContent.trim(), 10) || 0; return val(b, C.POINTS) - val(a, C.POINTS) || val(b, C.GOAL_DIFFERENCE) - val(a, C.GOAL_DIFFERENCE) || val(b, C.GOALS_FOR) - val(a, C.GOALS_FOR); }); rows.forEach((row, index) => { row.cells[C.POSITION].textContent = index + 1; tbody.appendChild(row); }); // YENİ EKLENEN SATIR: Sıralama sonrası çizgileri yeniden uygula. applyPromotionRelegationStyles(); } function toggleLogoVisibility() { if (isLogoDisplayEnabled) { addTeamLogos(); } else { removeTeamLogos(); } } function addTeamLogos() { if (!isLogoDisplayEnabled) return; document.querySelectorAll(CONFIG.SELECTORS.TABLE_ROWS).forEach(row => { const teamLink = row.querySelector(CONFIG.SELECTORS.TEAM_LINK_IN_ROW); if (teamLink && teamLink.parentNode && !row.querySelector(`.${CONFIG.CLASSES.TEAM_LOGO}`)) { const tid = new URLSearchParams(teamLink.href).get('tid'); if (tid) { const logo = document.createElement('img'); logo.src = `/dynimg/badge.php?team_id=${tid}&sport=soccer`; logo.className = CONFIG.CLASSES.TEAM_LOGO; logo.alt = 'Logo'; teamLink.parentNode.insertBefore(logo, teamLink); } } }); } function removeTeamLogos() { document.querySelectorAll(`.${CONFIG.CLASSES.TEAM_LOGO}`).forEach(logo => logo.remove()); } async function performFullOpponentAnalysis() { try { if (!myTeamInfo.name || !fullScheduleCache) return; const fixtureData = parseMyTeamFixture(fullScheduleCache.htmlText); applyAllVisuals(fixtureData); } catch (error) { console.error("Opponent analysis failed:", error); } } function applyAllVisuals({ fixture, nextOpponentName }) { if (!fixture) return; const tableBody = document.querySelector(CONFIG.SELECTORS.TABLE_BODY); if (!tableBody) return; document.querySelectorAll(`.${CONFIG.CLASSES.INDICATORS_WRAPPER}`).forEach(el => el.remove()); document.querySelectorAll(`.${CONFIG.CLASSES.NEXT_OPPONENT}`).forEach(el => el.classList.remove(CONFIG.CLASSES.NEXT_OPPONENT)); tableBody.querySelectorAll("tr").forEach(row => { const teamLink = row.querySelector(CONFIG.SELECTORS.TEAM_LINK_IN_ROW); if (!teamLink || row.classList.contains('highlight_row')) return; const teamCell = teamLink.parentNode; teamCell.classList.add('mz-team-cell'); const teamName = teamLink.textContent.trim(); const result = fixture[teamName]; if (nextOpponentName === teamName) row.classList.add(CONFIG.CLASSES.NEXT_OPPONENT); if (result) { const wrapper = document.createElement('span'); wrapper.className = CONFIG.CLASSES.INDICATORS_WRAPPER; if (result.played && result.played.length > 0) { result.played.forEach(playedMatch => { const icon = document.createElement('span'); icon.className = `match-indicator match-${playedMatch.status}`; icon.dataset.tooltipText = `${playedMatch.location} ${playedMatch.score}`; icon.dataset.status = playedMatch.status; icon.addEventListener('click', () => GM_openInTab(playedMatch.matchURL, true)); addTooltipEvents(icon); wrapper.appendChild(icon); }); } if (result.futureRounds.length > 0) { const text = document.createElement('span'); text.className = 'match-future'; text.textContent = `(${result.futureRounds.join(', ')})`; wrapper.appendChild(text); } if (wrapper.hasChildNodes()) teamCell.appendChild(wrapper); } }); } function addTooltipEvents(element) { element.addEventListener('mouseenter', (e) => { const target = e.currentTarget; const wrapperElement = target.closest(`.${CONFIG.CLASSES.INDICATORS_WRAPPER}`); const positioningElement = wrapperElement || target; const rect = positioningElement.getBoundingClientRect(); tooltip.className = `${CONFIG.CLASSES.TOOLTIP} tooltip-${target.dataset.status}`; tooltip.textContent = target.dataset.tooltipText; tooltip.style.display = 'block'; tooltip.style.top = `${rect.top + window.scrollY + rect.height / 2 - tooltip.offsetHeight / 2}px`; tooltip.style.left = `${rect.right + window.scrollX + 10}px`; }); element.addEventListener('mouseleave', () => { tooltip.style.display = 'none'; }); } function addFixtureDifficultyColumn() { const headerRow = document.querySelector(CONFIG.SELECTORS.TABLE_HEADER_ROW); if (!headerRow || headerRow.querySelector(`.${CONFIG.CLASSES.FZ_HEADER}`)) return; const newHeader = document.createElement('th'); newHeader.textContent = i18n.get('fixtureDifficultyHeader'); newHeader.title = i18n.get('fixtureDifficultyTitle'); newHeader.classList.add(CONFIG.CLASSES.FZ_HEADER); headerRow.insertBefore(newHeader, headerRow.children[CONFIG.COLUMNS.POINTS + 1]); document.querySelectorAll(CONFIG.SELECTORS.TABLE_ROWS).forEach(row => { const newCell = row.insertCell(CONFIG.COLUMNS.FIXTURE_DIFFICULTY); newCell.textContent = '...'; }); } async function fetchAndCacheSchedule() { if (fullScheduleCache) return fullScheduleCache; const scheduleTabLink = document.querySelector(CONFIG.SELECTORS.SCHEDULE_TAB_LINK); if (!scheduleTabLink) return null; try { const response = await request({ method: "GET", url: scheduleTabLink.href }); fullScheduleCache = { htmlText: response.responseText }; return fullScheduleCache; } catch (error) { console.error("Could not fetch schedule page:", error); return null; } } function parseMyTeamFixture(htmlText) { const doc = new DOMParser().parseFromString(htmlText, 'text/html'); const fixture = {}; let nextMatch = { round: Infinity, opponentName: null }; const roundRegex = /\d+/; doc.querySelectorAll(CONFIG.SELECTORS.LEAGUE_TABLE_HEADER).forEach(header => { const roundMatch = header.textContent.match(roundRegex); if (!roundMatch) return; const round = parseInt(roundMatch[0], 10); const table = header.nextElementSibling?.querySelector('table'); if (!table) return; table.querySelectorAll('tr').forEach(row => { const [homeCell, scoreCell, awayCell] = row.cells; if (!homeCell || !scoreCell || !awayCell) return; const homeTeam = homeCell.textContent.trim(); const awayTeam = awayCell.textContent.trim(); if (homeTeam === myTeamInfo.name || awayTeam === myTeamInfo.name) { const opponentName = homeTeam === myTeamInfo.name ? awayTeam : homeTeam; if (!fixture[opponentName]) fixture[opponentName] = { played: [], futureRounds: [] }; const scoreLink = scoreCell.querySelector('a'); const mid = scoreLink ? new URLSearchParams(scoreLink.href).get('mid') : null; const location = homeTeam === myTeamInfo.name ? i18n.get('locationHome') : i18n.get('locationAway'); const currentRoundText = `${i18n.get('roundPrefix')} ${round}`; if (mid && liveMatchResults.has(mid)) { const d = liveMatchResults.get(mid); const myScore = d.homeTid === myTeamInfo.id ? d.homeGoals : d.awayGoals; const oppScore = d.homeTid === myTeamInfo.id ? d.awayGoals : d.homeGoals; fixture[opponentName].played.push({ score: `${myScore} - ${oppScore}`, matchURL: scoreLink.href, location: location, status: myScore > oppScore ? 'win' : (myScore < oppScore ? 'loss' : 'draw'), }); } else if (scoreLink && /^\d+\s*-\s*\d+$/.test(scoreLink.textContent.trim())) { const scores = scoreLink.textContent.trim().split('-').map(s => parseInt(s.trim())); const myScore = homeTeam === myTeamInfo.name ? scores[0] : scores[1]; const oppScore = homeTeam === myTeamInfo.name ? scores[1] : scores[0]; fixture[opponentName].played.push({ score: `${myScore} - ${oppScore}`, matchURL: scoreLink.href, location: location, status: myScore > oppScore ? 'win' : (myScore < oppScore ? 'loss' : 'draw'), }); } else { fixture[opponentName].futureRounds.push(currentRoundText); if (round < nextMatch.round) nextMatch = { round, opponentName }; } } }); }); return { fixture, nextOpponentName: nextMatch.opponentName }; } // DEĞİŞİKLİK BAŞLANGICI: Evrenselleştirilmiş Fonksiyon function parseScheduleForAllTeams(htmlText, newlyFinishedMatches = new Map()) { const doc = new DOMParser().parseFromString(htmlText, 'text/html'); const allFixtures = {}; document.querySelectorAll(CONFIG.SELECTORS.TABLE_ROWS).forEach(row => { const teamLink = row.querySelector(CONFIG.SELECTORS.TEAM_LINK_IN_ROW); if (teamLink) allFixtures[teamLink.textContent.trim()] = { futureOpponents: [] }; }); const matchRows = doc.querySelectorAll('table.hitlist tbody tr, .matches_container table tbody tr'); matchRows.forEach(row => { if (row.cells.length < 3) return; const scoreCell = row.cells[1]; const scoreText = scoreCell.textContent.trim(); const scoreLink = scoreCell.querySelector('a[href*="mid="]'); const mid = scoreLink ? new URLSearchParams(scoreLink.href).get('mid') : null; // ESKİ HALİ (Kırılgan): if ((!/^\d+\s*-\s*\d+$/.test(scoreText) || scoreText.includes('vs')) && !newlyFinishedMatches.has(mid)) // YENİ HALİ (Evrensel): Oynanmamış bir maçı tespit etmek için sadece skor formatını kontrol etmek yeterlidir. // Bu, 'vs', 'gegen', 'X-X' veya diğer dil varyasyonlarını otomatik olarak yakalar. if (!/^\d+\s*-\s*\d+$/.test(scoreText) && !newlyFinishedMatches.has(mid)) { const homeTeam = row.cells[0].textContent.trim(); const awayTeam = row.cells[2].textContent.trim(); if (allFixtures[homeTeam] && allFixtures[awayTeam]) { allFixtures[homeTeam].futureOpponents.push(awayTeam); allFixtures[awayTeam].futureOpponents.push(homeTeam); } } }); return allFixtures; } // DEĞİŞİKLİK SONU async function calculateAndDisplayFixtureDifficulty(newlyFinishedMatches = new Map()) { const scheduleData = await fetchAndCacheSchedule(); if (!scheduleData) { document.querySelectorAll(`td:nth-child(${CONFIG.COLUMNS.FIXTURE_DIFFICULTY + 1})`).forEach(cell => { if (cell) { cell.textContent = '-'; cell.title = i18n.get('fzNotAvailable'); } }); return; } const allFixtures = parseScheduleForAllTeams(scheduleData.htmlText, newlyFinishedMatches); const teamRankings = new Map(); document.querySelectorAll(CONFIG.SELECTORS.TABLE_ROWS).forEach(row => { const teamLink = row.querySelector(CONFIG.SELECTORS.TEAM_LINK_IN_ROW); const rank = parseInt(row.cells[CONFIG.COLUMNS.POSITION].textContent, 10); if (teamLink && !isNaN(rank)) teamRankings.set(teamLink.textContent.trim(), rank); }); document.querySelectorAll(CONFIG.SELECTORS.TABLE_ROWS).forEach(row => { const teamLink = row.querySelector(CONFIG.SELECTORS.TEAM_LINK_IN_ROW); if (!teamLink) return; const teamName = teamLink.textContent.trim(); const teamFixture = allFixtures[teamName]; const fzCell = row.cells[CONFIG.COLUMNS.FIXTURE_DIFFICULTY]; if (fzCell && teamFixture && teamFixture.futureOpponents.length > 0) { const rankSum = teamFixture.futureOpponents.reduce((sum, opponentName) => sum + (teamRankings.get(opponentName) || 0), 0); const fzValue = rankSum / teamFixture.futureOpponents.length; fzCell.textContent = fzValue.toFixed(1); fzCell.title = i18n.get('fzTooltip', { remaining: teamFixture.futureOpponents.length, rankSum: rankSum }); } else if (fzCell) { fzCell.textContent = '-'; fzCell.title = i18n.get('fzNoMatchesLeft'); } }); } // ================================================================================= // BÖLÜM 2.3: İÇ SAHA / DIŞ SAHA FİLTRELEME FONKSİYONLARI // ================================================================================= /** * Maç programı sayfasındaki tüm OYNANMIŞ maçları ayrıştırır ve bir dizi olarak döndürür. * Sonuçları `allPlayedMatchesCache` içinde saklar. */ function parseAllPlayedMatches() { if (allPlayedMatchesCache) return allPlayedMatchesCache; if (!fullScheduleCache || !fullScheduleCache.htmlText) return []; const doc = new DOMParser().parseFromString(fullScheduleCache.htmlText, 'text/html'); const playedMatches = []; const scoreRegex = /^\d+\s*-\s*\d+$/; // Fikstür sayfalarındaki tüm maç satırlarını bul doc.querySelectorAll('.matches_container table tbody tr, table.hitlist tbody tr').forEach(row => { const cells = row.cells; if (cells.length < 3) return; const homeTeam = cells[0].textContent.trim(); const scoreText = cells[1].textContent.trim(); const awayTeam = cells[2].textContent.trim(); // Sadece skoru olan (oynanmış) maçları işle if (scoreRegex.test(scoreText)) { const scores = scoreText.split('-').map(s => parseInt(s.trim(), 10)); playedMatches.push({ homeTeam, awayTeam, homeGoals: scores[0], awayGoals: scores[1] }); } }); allPlayedMatchesCache = playedMatches; return playedMatches; } /** * Verilen maç listesine ve filtre türüne göre puan durumunu hesaplar. * @param {string[]} allTeams - Ligdeki tüm takımların adlarını içeren dizi. * @param {object[]} allMatches - Oynanmış tüm maçların verilerini içeren dizi. * @param {'home'|'away'} filterType - Hesaplanacak tablo türü. * @returns {object} - Her takım için hesaplanmış istatistikleri içeren nesne. */ function calculateTableFromMatches(allTeams, allMatches, filterType) { const standings = {}; // Tüm takımlar için istatistikleri sıfırla allTeams.forEach(team => { standings[team] = { p: 0, w: 0, d: 0, l: 0, gf: 0, ga: 0, played: 0 }; }); allMatches.forEach(match => { const { homeTeam, awayTeam, homeGoals, awayGoals } = match; // Maç sonucunu belirle let homeResult, awayResult; if (homeGoals > awayGoals) { homeResult = { p: 3, w: 1, d: 0, l: 0 }; awayResult = { p: 0, w: 0, d: 0, l: 1 }; } else if (awayGoals > homeGoals) { homeResult = { p: 0, w: 0, d: 0, l: 1 }; awayResult = { p: 3, w: 1, d: 0, l: 0 }; } else { homeResult = { p: 1, w: 0, d: 1, l: 0 }; awayResult = { p: 1, w: 0, d: 1, l: 0 }; } // Filtreye göre istatistikleri güncelle if (filterType === 'home' && standings[homeTeam]) { standings[homeTeam].played++; standings[homeTeam].p += homeResult.p; standings[homeTeam].w += homeResult.w; standings[homeTeam].d += homeResult.d; standings[homeTeam].l += homeResult.l; standings[homeTeam].gf += homeGoals; standings[homeTeam].ga += awayGoals; } if (filterType === 'away' && standings[awayTeam]) { standings[awayTeam].played++; standings[awayTeam].p += awayResult.p; standings[awayTeam].w += awayResult.w; standings[awayTeam].d += awayResult.d; standings[awayTeam].l += awayResult.l; standings[awayTeam].gf += awayGoals; standings[awayTeam].ga += homeGoals; } }); return standings; } /** * Hesaplanan yeni puan durumu verileriyle DOM'daki tabloyu günceller. * @param {object} standings - Hesaplanmış istatistikleri içeren nesne. */ function updateTableDisplay(standings) { const tableRows = document.querySelectorAll(CONFIG.SELECTORS.TABLE_ROWS); tableRows.forEach(row => { const teamLink = row.querySelector(CONFIG.SELECTORS.TEAM_LINK_IN_ROW); if (!teamLink) return; const teamName = teamLink.textContent.trim(); const stats = standings[teamName]; if (!stats) return; const C = CONFIG.COLUMNS; const cells = row.cells; cells[C.PLAYED].textContent = stats.played; cells[C.WINS].textContent = stats.w; cells[C.DRAWS].textContent = stats.d; cells[C.LOSSES].textContent = stats.l; cells[C.GOALS_FOR].textContent = stats.gf; cells[C.GOALS_AGAINST].textContent = stats.ga; cells[C.GOAL_DIFFERENCE].textContent = stats.gf - stats.ga; cells[C.POINTS].textContent = stats.p; }); sortTableByPoints(); } /** * Filtre butonlarını oluşturur ve sayfaya ekler. */ function setupHomeAwayFilters() { const titleHeader = findMainLeagueTableHeader(); if (!titleHeader || document.querySelector('.mz-table-filters')) return; // Önceki denemelerden kalmış olabilecek eski konteynerleri temizle const existingFilters = titleHeader.querySelector('.mz-table-filters'); if (existingFilters) existingFilters.remove(); const filterContainer = document.createElement('div'); filterContainer.className = 'mz-table-filters'; filterContainer.style.cssText = 'display: inline-flex; gap: 5px; margin-left: 15px; vertical-align: middle;'; const filters = [ { type: 'all', text: i18n.get('overall') }, { type: 'home', text: i18n.get('home') }, { type: 'away', text: i18n.get('away') } ]; filters.forEach(filter => { const buttonLink = document.createElement('a'); buttonLink.href = '#'; buttonLink.className = 'mzbtn buttondiv button_account'; buttonLink.dataset.filter = filter.type; buttonLink.style.margin = '0'; // YENİ EKLENDİ: Butonun büyüme animasyonunu akıcı hale getirir. buttonLink.style.transition = 'transform 0.15s ease-in-out'; buttonLink.innerHTML = ` <span class="buttonClassMiddle"> <span style="white-space: nowrap;">${filter.text}</span> </span> <span class="buttonClassRight"> </span>`; if (filter.type === 'all') { // ESKİ HALİ: buttonLink.style.filter = 'brightness(85%)'; // YENİ HALİ: Sayfa ilk açıldığında "Genel" butonu aktif ve büyük görünür. buttonLink.style.transform = 'scale(1.05)'; buttonLink.style.zIndex = '1'; // Diğer butonların üzerine çıkması için } buttonLink.addEventListener('click', (e) => { e.preventDefault(); handleFilterClick(filter.type); }); filterContainer.appendChild(buttonLink); }); titleHeader.appendChild(filterContainer); } /** * Filtre butonuna tıklandığında tetiklenir. (Güncellenmiş Versiyon) */ function handleFilterClick(filterType) { // Aktif buton stilini ayarla document.querySelectorAll('.mz-table-filters .mzbtn').forEach(btn => { // ESKİ MANTIK (Karartma): // btn.style.filter = ''; // if (btn.dataset.filter === filterType) { // btn.style.filter = 'brightness(85%)'; // } // YENİ MANTIK (Büyütme): // Önce tüm butonları varsayılan (küçük) haline geri getir. btn.style.transform = 'scale(1)'; btn.style.zIndex = '0'; // Sadece tıklanan butonu büyüt ve öne çıkar. if (btn.dataset.filter === filterType) { btn.style.transform = 'scale(1.15)'; // Hafifçe büyüt btn.style.zIndex = '1'; // Diğer butonların üzerine çıkmasını sağla } }); const currentTbody = document.querySelector(CONFIG.SELECTORS.TABLE_BODY); if (!currentTbody || !originalTableBody) return; if (filterType === 'all') { currentTbody.parentNode.replaceChild(originalTableBody.cloneNode(true), currentTbody); addFixtureDifficultyColumn(); toggleLogoVisibility(); calculateAndDisplayFixtureDifficulty(); if (myTeamInfo.id) { performFullOpponentAnalysis(); } return; } const allMatches = parseAllPlayedMatches(); if (!allMatches || allMatches.length === 0) return; const allTeams = Array.from(originalTableBody.querySelectorAll(CONFIG.SELECTORS.TEAM_LINK_IN_ROW)) .map(link => link.textContent.trim()); const newStandings = calculateTableFromMatches(allTeams, allMatches, filterType); updateTableDisplay(newStandings); } // BÖLÜM 2.4: ELO SCORE FONKSİYONLARI // ================================================================================= async function addEloScores() { // 1. KİLİT KONTROLÜ: Fonksiyon zaten çalışıyorsa, tekrar çalıştırmadan çık. if (isAddingEloScores) { return; } const leagueTable = document.querySelector(CONFIG.SELECTORS.LEAGUE_TABLE); // 2. MEVCUT KONTROL: Eğer tablo veya zaten eklenmiş bir ELO başlığı varsa, tekrar çalışma. if (!leagueTable || leagueTable.querySelector('.elo-score-header')) { return; } // İŞLEMİ KİLİTLE isAddingEloScores = true; try { const teamIds = []; const tableRows = leagueTable.querySelectorAll('tbody tr'); const teamIdMap = new Map(); tableRows.forEach(row => { const teamLink = row.querySelector('a[href*="&tid="]'); if (teamLink) { const tid = new URLSearchParams(teamLink.href).get('tid'); if (tid) { teamIds.push(tid); teamIdMap.set(tid, row); } } }); if (teamIds.length === 0) return; // return'den önce kilidi açmak için finally'ye gidecek const apiUrl = `https://statsxente.com/MZ1/Functions/tamper_teams.php?sport=soccer${teamIds.map((id, i) => `&idEquipo${i}=${id}`).join('')}`; const response = await request({ method: "GET", url: apiUrl }); if (!response || !response.responseText) { console.warn("ELO API'sinden boş yanıt alındı. Bu lig için ELO verisi mevcut değil."); return; } const eloData = JSON.parse(response.responseText); if (Object.keys(eloData).length === 0) { console.warn("ELO API'sinden veri geldi ancak bu takımlar için ELO puanı bulunamadı."); return; } const headerRow = leagueTable.querySelector('thead tr'); if (!headerRow) return; const newHeader = document.createElement('th'); newHeader.textContent = 'ELO'; newHeader.title = 'ELO Puanı'; newHeader.className = 'elo-score-header'; headerRow.appendChild(newHeader); tableRows.forEach(row => { const newCell = row.insertCell(-1); newCell.textContent = '-'; newCell.className = 'elo-score-cell'; }); for (const teamId in eloData) { const row = teamIdMap.get(teamId); if (row && eloData[teamId]?.elo) { const eloScore = Math.round(eloData[teamId].elo); const eloCell = row.cells[row.cells.length - 1]; if (eloCell) { eloCell.textContent = eloScore.toLocaleString('de-DE'); } } } } catch (error) { console.error("ELO verileri işlenirken bir hata oluştu:", error); } finally { // İŞLEM BİTTİĞİNDE VEYA HATA OLDUĞUNDA KİLİDİ AÇ isAddingEloScores = false; } } let isRunningRun = false; async function run() { if (isRunningRun || !isMainLeagueTableVisible()) { return; } isRunningRun = true; try { if (!originalTableBody) { const tbody = document.querySelector(CONFIG.SELECTORS.TABLE_BODY); if (tbody) originalTableBody = tbody.cloneNode(true); } toggleLogoVisibility(); const scheduleTabLink = document.querySelector(CONFIG.SELECTORS.SCHEDULE_TAB_LINK); if (scheduleTabLink) { addFixtureDifficultyColumn(); await fetchAndCacheSchedule(); await calculateAndDisplayFixtureDifficulty(); } const myTeamRow = document.querySelector(CONFIG.SELECTORS.MY_TEAM_ROW); const myTeamLink = myTeamRow?.querySelector(CONFIG.SELECTORS.TEAM_LINK_IN_ROW); if (myTeamLink) { myTeamInfo.name = myTeamLink.textContent.trim(); myTeamInfo.id = new URLSearchParams(myTeamLink.href).get('tid'); } if (myTeamInfo.id) { await performFullOpponentAnalysis(); } setupLiveUpdateFeature(); setupSettingsMenu(); toggleHomeAwayFiltersVisibility(); // owned by factual-insights } finally { isRunningRun = false; } } async function initializeScript() { isLogoDisplayEnabled = await GM_getValue('showTeamLogos', true); // <<< BU SATIRI EKLEYİN >>> isHomeAwayFiltersEnabled = await GM_getValue('showHomeAwayFilters', true); injectStyles(); createSettingsModal(); NProgress.configure({ showSpinner: false }); const contentArea = document.querySelector(CONFIG.SELECTORS.CONTENT_DIV); if (!contentArea) return; const observer = new MutationObserver(debounce((mutations) => { if (isMainLeagueTableVisible() && !document.querySelector(`.${CONFIG.CLASSES.FZ_HEADER}`)) { fullScheduleCache = null; originalTableBody = null; run(); } }, 800)); observer.observe(contentArea, { childList: true, subtree: true }); run(); } initializeScript(); } const RETIREMENT_DATA_KEY = 'mz_retirement_data_v2'; /**************************************************************************************** * * * BÖLÜM 3: OYUNCU İSTATİSTİK BETİĞİ (ManagerZone Player Stat Averages) * * * ****************************************************************************************/ function initializePlayerStatsScript() { // --- INTERNATIONALIZATION (i18n) --- const translations = { tr: { // UI Buttons & Titles autoScan: "Otomatik Tara", statistics: "İstatistikler", compare: "Karşılaştır", clearData: "Verileri Sil", compareAllPlayers: "Tüm Oyuncuları Karşılaştır", autoScanOptions: "Otomatik Tarama Seçenekleri", playerComparison: "Oyuncu Karşılaştırması", scanSelectedCategories: (count) => `Seçilen ${count} Kategoride Taramayı Başlat`, avgStatsForMatches: (count) => `Ortalama İstatistikler (${count} maç)`, exportToExcel: "Excel'e Aktar", exporting: "Dışa aktarılıyor...", backupData: "Verileri Yedekle (JSON)", restoreData: "Yedekten Geri Yükle (JSON)", backupInProgress: "Yedekleniyor...", restoreInProgress: "Geri Yüklüyorum...", restoreSuccess: "Veriler başarıyla geri yüklendi! Tablo yenileniyor...", restoreError: "Geçersiz yedek dosyası! Lütfen betik tarafından oluşturulmuş bir JSON dosyası seçtiğinizden emin olun.", // UI Texts & Placeholders scanInfo: "Aşağıdan istatistiklerini toplamak istediğiniz maç türlerini seçin.", selectAll: "Tümünü Seç", deselectAll: "Seçimi Kaldır", // Popups & Alerts loadingPlayerList: "Oyuncu listeniz oluşturuluyor, lütfen bekleyin...", noDataFound: "Karşılaştırılacak oyuncu veya istatistik bulunamadı. Lütfen önce 'Oyuncular' sayfanızı ziyaret ederek listenizi oluşturun veya bir tarama yapın.", confirmClearData: "Tüm oyuncu istatistikleri ve taranan maç kayıtları kalıcı olarak silinecek. Emin misiniz?", allDataCleared: "Tüm istatistik verileri başarıyla temizlendi.", noMatchTypes: "Taranacak maç tipi bulunamadı.", confirmScan: (count) => `${count} farklı maç tipi taranacak. Bu işlem arka planda çalışacak ve biraz zaman alabilir. Devam edilsin mi?`, scanStarting: "Tarama başlıyor...", scanningCategory: (name, current, total) => `Kategori taranıyor: ${name} (${current}/${total})`, processingNewMatch: (catIndex, catTotal, category, matchCurrent, matchTotal, totalScanned, totalOverall) => `Kategori: ${catIndex}/${catTotal} (${category}) | Maç: ${matchCurrent}/${matchTotal} | Toplam: ${totalScanned}/${totalOverall}`, errorFetchingMatch: (mid) => `Hata: Maç ID ${mid} çekilemedi.`, errorProcessingCategory: (name) => `Hata: ${name} işlenemedi. Sonrakine geçiliyor...`, scanFinishedVerifying: "Tarama bitti, kadro listesi doğrulanıyor...", scanSuccessNewMatches: "Otomatik tarama başarıyla tamamlandı! Yeni maç istatistikleri eklendi.", scanSuccessNoNewMatches: "Tarama tamamlandı. Seçilen kategorilerde taranacak yeni maç bulunamadı.", noPlayersToShow: "Gösterilecek oyuncu bulunamadı.", noMatchesForCriteria: "Seçili kriterlere uygun maç bulunamadı.", exportError: "Excel'e aktarma sırasında bir hata oluştu.", // Filters & Table Headers all: "Tümü", none: "Yok", league: "Lig", under18: "Altı 18", under21: "Altı 21", under23: "Altı 23", player: "Oyuncu", age: "Yaş", matches: "Maç", totalSuffix: "(Toplam)", avgSuffix: "(Ort.)", // Stat Mappings (Türkçe) statMappings: { 'N': 'Ortalama Puan', 'MP': 'Oynadığı Süre (dk)', 'G': 'Gol', 'A': 'Asist', 'SOT': 'Kaleyi Bulan Şut', 'SOT%': 'Kaleyi Bulan Şut Yüzdesi', 'P%': 'Başarılı Pas Yüzdesi', 'PL': 'Ortalama Başarılı Pas Mesafesi (m)', 'In': 'Akın Kesme', 'T': 'Top Çalma', 'T%': 'Top Çalma Yüzdesi', 'Pos%': 'Tüm takım içinde topa sahip olma yüzdesi', 'Dist': 'Kat Edilen Mesafe (km)', 'DistP': 'Topla toplam katedilen mesafesi (km)', 'SpP': 'Topla ortalama hızı (km/s)', 'SpR': 'Koşarken ortalama hızı (km/s)', 'Pen': 'Penaltı', 'YC': 'Sarı Kart', 'RC': 'Kırmızı Kart', 'SV': 'Kurtarış' }, // DEĞİŞİKLİK: Kategori değerlerini ve metinlerini eşleştirmek için categoryValueMap: { 'league_senior': 'Lig', 'series': 'Lig', 'friendlyseries': 'Dostluk Ligi', 'world_series': 'Dünya Ligi', 'friendly': 'Hazırlık Maçı', 'cup': 'Resmi Kupa', 'u18_world_series': 'U18 Dünya Ligi', 'u21_world_series': 'U21 Dünya Ligi', 'u23_world_series': 'U23 Dünya Ligi', 'private_cup': 'Ödüllü/Dostluk Kupası' } }, en: { // UI Buttons & Titles autoScan: "Auto-Scan", statistics: "Statistics", compare: "Compare", clearData: "Clear Data", compareAllPlayers: "Compare All Players", autoScanOptions: "Auto-Scan Options", playerComparison: "Player Comparison", scanSelectedCategories: (count) => `Start Scan for ${count} Selected Categories`, avgStatsForMatches: (count) => `Average Statistics (${count} matches)`, exportToExcel: "Export to Excel", exporting: "Exporting...", backupData: "Backup Data (JSON)", restoreData: "Restore from Backup (JSON)", backupInProgress: "Backing up...", restoreInProgress: "Restoring...", restoreSuccess: "Data restored successfully! Refreshing table...", restoreError: "Invalid backup file! Please make sure you select a JSON file created by the script.", // UI Texts & Placeholders scanInfo: "Select the match types you want to collect statistics from below.", selectAll: "Select All", deselectAll: "Deselect All", // Popups & Alerts loadingPlayerList: "Creating your player list, please wait...", noDataFound: "No players or statistics found to compare. Please visit your 'Squad' page first to create your list or perform a scan.", confirmClearData: "All player statistics and scanned match records will be permanently deleted. Are you sure?", allDataCleared: "All statistics data has been successfully cleared.", noMatchTypes: "No match types found to scan.", confirmScan: (count) => `${count} different match types will be scanned. This process will run in the background and may take some time. Do you want to continue?`, scanStarting: "Scan is starting...", scanningCategory: (name, current, total) => `Scanning category: ${name} (${current}/${total})`, processingNewMatch: (catIndex, catTotal, category, matchCurrent, matchTotal, totalScanned, totalOverall) => `Category: ${catIndex}/${catTotal} (${category}) | Match: ${matchCurrent}/${matchTotal} | Total: ${totalScanned}/${totalOverall}`, errorFetchingMatch: (mid) => `Error: Could not fetch match ID ${mid}.`, errorProcessingCategory: (name) => `Error: Could not process ${name}. Skipping to the next one...`, scanFinishedVerifying: "Scan finished, verifying squad list...", scanSuccessNewMatches: "Automated scan completed successfully! New match statistics have been added.", scanSuccessNoNewMatches: "Scan completed. No new matches were found to scan in the selected categories.", noPlayersToShow: "No players to display.", noMatchesForCriteria: "No matches found for the selected criteria.", exportError: "An error occurred during the Excel export.", // Filters & Table Headers all: "All", none: "None", league: "League", under18: "Under 18", under21: "Under 21", under23: "Under 23", player: "Player", age: "Age", matches: "Matches", totalSuffix: "(Total)", avgSuffix: "(Avg.)", // Stat Mappings (English) - Based on the provided HTML statMappings: { 'MP': 'Minutes played', 'N': 'Rating', 'G': 'Goals', 'A': 'Assists', 'S': 'Shots', 'G%': 'Goal percentage', 'SOT': 'Shots on target', 'SOT%': 'Shots on target percentage', 'P%': 'Successful pass percentage', 'PL': 'Average successful pass length (m)', 'In': 'Interceptions', 'T': 'Tackles', 'T%': 'Successful tackle percentage', 'Pos%': 'Team possession percentage', 'Dist': 'Distance covered (km)', 'DistP': 'Distance covered with ball (km)', 'SpP': 'Average speed with ball (km/h)', 'SpR': 'Average running speed (km/h)', 'Pen': 'Penalties', 'YC': 'Yellow Cards', 'RC': 'Red Cards', 'SV': 'Saves' }, // DEĞİŞİKLİK: Kategori değerlerini ve metinlerini eşleştirmek için categoryValueMap: { 'league_senior': 'League', 'series': 'League', 'friendlyseries': 'Friendly League', 'world_series': 'World League', 'friendly': 'Friendly', 'cup': 'Official Cup', 'u18_world_series': 'U18 World League', 'u21_world_series': 'U21 World League', 'u23_world_series': 'U23 World League', 'private_cup': 'Prized/Friendly Cup' } } }; const detectedLang = $('meta[name="language"]').attr('content') || 'en'; const lang = detectedLang === 'tr' ? 'tr' : 'en'; const i18n = translations[lang]; const comparisonIsSpanish = String(detectedLang).toLowerCase().startsWith('es'); Object.assign(i18n, comparisonIsSpanish ? { autoScan: 'Escanear estadísticas', compare: 'Comparar', compareAllPlayers: 'Comparar todos los jugadores', autoScanOptions: 'Opciones de escaneo automático', playerComparison: 'Comparación de jugadores', scanSelectedCategories: (count) => `Escanear ${count} categorías seleccionadas`, exportToExcel: 'Exportar a Excel', exporting: 'Exportando...', backupData: 'Respaldar datos (JSON)', restoreData: 'Restaurar respaldo (JSON)', scanInfo: 'Selecciona los tipos de partido cuyas estadísticas quieres recopilar.', selectAll: 'Seleccionar todo', deselectAll: 'Deseleccionar todo', loadingPlayerList: 'Cargando los jugadores de tu plantel...', noDataFound: 'No se encontraron jugadores en la página actual.', noMatchTypes: 'No se encontraron tipos de partido para escanear.', scanStarting: 'Iniciando el escaneo...', scanningCategory: (name, current, total) => `Escaneando ${name} (${current}/${total})`, processingNewMatch: (catIndex, catTotal, category, matchCurrent, matchTotal, totalScanned, totalOverall) => `Categoría ${catIndex}/${catTotal} (${category}) · Partido ${matchCurrent}/${matchTotal} · Total ${totalScanned}/${totalOverall}`, errorFetchingMatch: (mid) => `No se pudo cargar el partido ${mid}.`, errorProcessingCategory: (name) => `No se pudo procesar ${name}; continuando con la siguiente categoría.`, scanFinishedVerifying: 'Escaneo finalizado; actualizando el plantel...', noMatchesForCriteria: 'No hay partidos para los filtros seleccionados.', exportError: 'Ocurrió un error al exportar a Excel.', all: 'Todos', none: 'Ninguno', under18: 'Sub-18', under21: 'Sub-21', under23: 'Sub-23', player: 'Jugador', age: 'Edad', matches: 'Partidos', statsNeedScan: 'Los jugadores ya están cargados desde esta página. Escanea las estadísticas de partidos una vez para crear la comparación.', scanMatchStats: 'Escanear estadísticas de partidos' } : { statsNeedScan: 'Players are already loaded from this page. Scan match statistics once to build the comparison.', scanMatchStats: 'Scan match statistics' }); function getShortCategoryName(categoryText) { // DEĞİŞİKLİK BAŞLANGICI: i18n'den gelen metinlerle çalış const trMap = translations.tr.categoryValueMap; const enMap = translations.en.categoryValueMap; // Metnin hangi haritadan geldiğini bul if (Object.values(trMap).includes(categoryText)) { if (categoryText === "Federasyon Çatışması Gözlemci Maçı") return "Fed. Gözlemci"; if (categoryText === "Federasyon Çatışması Mücadelesi") return "Fed. Mücadelesi"; if (categoryText === "Ödüllü/Dostluk Kupası") return "Ödüllü/Dostluk"; } else if (Object.values(enMap).includes(categoryText)) { if (categoryText === "Federation Clash Scout Match") return "Fed. Scout"; if (categoryText === "Federation Clash Challenge") return "Fed. Challenge"; if (categoryText === "Prized/Friendly Cup") return "Prized/Friendly"; if (categoryText === "Federationsclash: Scoutmatch") return "Fed. Scout"; if (categoryText === "Federationsclash: Utmaning") return "Fed. Utmaning"; } return categoryText; // DEĞİŞİKLİK SONU } // Global variable let squadPlayersWithStats = []; let userTeamId = null; // --- Helper Functions --- const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // --- Style Definitions --- GM_addStyle(` .mz-script-button { padding: 8px 15px; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; font-weight: bold; color: white; text-align: center; transition: background-color 0.2s, opacity 0.2s; vertical-align: middle; margin: 5px; } .mz-script-button:disabled { background-color: #6c757d !important; opacity: 0.7; cursor: not-allowed; } .mz-script-button:hover:not(:disabled) { filter: brightness(110%); } #comparePlayersButton { background-color: #17a2b8; } #player-stats-popup, #player-comparison-popup, #scrape-options-popup { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: #f8f9fa; border: 2px solid #343a40; border-radius: 10px; padding: 15px; z-index: 10000; box-shadow: 0 5px 15px rgba(0,0,0,0.3); color: #212529; font-family: Dosis, sans-serif; width: 95vw; max-width: 1500px; max-height: 85vh; overflow: hidden; display: flex; flex-direction: column; box-sizing: border-box; } .popup-content-scrollable { overflow-y: auto; flex-grow: 1; padding-right: 5px; } .popup-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #dee2e6; padding-bottom: 10px; margin-bottom: 15px; flex-shrink: 0; } .popup-header h3 { margin: 0; color: #00529B; font-size: 1.1rem; } .popup-filters { display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; flex-shrink: 0; align-items: center; } .popup-filters select, #player-switcher { padding: 8px; border-radius: 4px; border: 1px solid #ccc; background-color: white; } #player-switcher { margin-right: 15px; } #player-stats-popup table, #player-comparison-popup table { width: 100%; border-collapse: collapse; font-size: 13px; } #player-stats-popup th, #player-stats-popup td, #player-comparison-popup th, #player-comparison-popup td { border: 1px solid #dee2e6; padding: 8px; text-align: left; white-space: nowrap; } #player-stats-popup th, #player-comparison-popup th { background-color: #e9ecef; } [data-tooltip] { cursor: help !important; } #player-comparison-popup .sort-asc::after { content: ' ▲'; color: #007bff; } #player-comparison-popup .sort-desc::after { content: ' ▼'; color: #007bff; } #player-comparison-popup .sort-sum-desc::after { content: ' (T ▼)'; color: #dc3545; } #player-comparison-popup .sort-sum-asc::after { content: ' (T ▲)'; color: #dc3545; } #player-comparison-popup .sort-avg-desc::after { content: ' (O ▼)'; color: #28a745; } #player-comparison-popup .sort-avg-asc::after { content: ' (O ▲)'; color: #28a745; } .table-responsive-wrapper { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; border: 1px solid #ddd; border-radius: 5px; } .popup-close { font-size: 28px; font-weight: bold; cursor: pointer; color: #adb5bd; line-height: 1; } #age-slider-filter-container { display: flex; align-items: center; gap: 10px; padding: 5px 0; width: 250px; /* Genişliği diğer filtrelere göre ayarlayabilirsiniz */ } #age-slider-filter-container label { font-weight: bold; color: #333; flex-shrink: 0; margin-bottom: 0; /* popup-filters'tan gelen stili ezer */ } #age-slider-filter-container input.age-slider-input { width: 45px; text-align: center; border: 1px solid #ccc; border-radius: 4px; padding: 5px; } #age-slider-range { flex-grow: 1; } /* jQuery UI Slider Stilleri (Oyunun stiliyle uyumlu) */ .ui-slider { position: relative; text-align: left; } .ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; touch-action: none; border-radius: 50%; background: #f6f6f6; border: 1px solid #ccc; outline: none; } .ui-slider .ui-slider-handle:hover { border-color: #999; } .ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; background-color: #007bff; /* Mavi renk aralık için */ } .ui-widget-content .ui-slider-range { background-color: #007bff; } .ui-slider-horizontal { height: .8em; } .ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; } .ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; } .ui-slider-horizontal .ui-slider-range-min { left: 0; } .ui-slider-horizontal .ui-slider-range-max { right: 0; } #mz-stat-loader { position: fixed; top: 0; left: 0; width: 100%; background: #28a745; color: white; padding: 10px 0; z-index: 10001; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.2); text-align: center; font-size: 16px; display: none; } #custom-tooltip { position: fixed; display: none; background-color: #222; color: white; padding: 8px 12px; border-radius: 5px; z-index: 10005; font-size: 12px; max-width: 250px; text-align: center; pointer-events: none; } .colored-cell { color: black; font-weight: bold; } #mz-stats-actions-container { display: inline-flex; gap: 8px; margin-left: 15px; vertical-align: middle; flex-wrap: wrap; justify-content: center; } #mz-stats-actions-container button { padding: 8px 12px; border: none; border-radius: 5px; cursor: pointer; font-size: 12px; font-weight: bold; color: white; text-align: center; transition: opacity 0.2s, background-color 0.2s; } #mz-stats-actions-container button:disabled { background-color: #6c757d !important; opacity: 0.7; cursor: not-allowed; } #scrapeStatsButton { background-color: #007bff; } #viewPlayerStatsButton { background-color: #fd7e14; } #comparePlayersButtonMatchPage { background-color: #17a2b8; } #clearAllStatsButton { background-color: #dc3545; } #scrape-options-list { list-style-type: none; padding: 0; margin: 0 0 15px 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; max-height: 300px; overflow-y: auto; } #scrape-options-list li { background: #e9ecef; padding: 8px; border-radius: 4px; } #scrape-options-list label { display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; } .scrape-options-controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } #start-scrape-button { background-color: #28a745; color: white; padding: 12px 20px; font-size: 16px; border: none; border-radius: 5px; cursor: pointer; width: 100%; } #player-stats-popup { width: auto; min-width: 500px; } #popup-stats-table { margin: 0 auto; } .dynamic-stats-container { position: relative; display: inline-flex; align-items: center; vertical-align: middle; margin-left: 4px; white-space: nowrap; } .stats-display-area { display: inline-flex; align-items: center; gap: 5px; cursor: pointer; font-size: 11px; } .stats-display-area .category-name { font-weight: bold; color: #00529B; } .stats-display-area .stats-values span { display: inline-flex; align-items: center; gap: 2px; margin-left: 4px; } .stats-display-area img { width: 10px; height: 10px; vertical-align: middle; } .assist-icon { font-family: "Arial", sans-serif; font-weight: bold; font-size: 14px; color: #28a745; vertical-align: middle; line-height: 1; } .stats-category-dropdown { display: none; position: absolute; top: 100%; left: 0; background-color: white; border: 1px solid #ccc; border-radius: 4px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); z-index: 10010; list-style-type: none; padding: 5px 0; margin: 2px 0 0 0; max-height: 200px; overflow-y: auto; } .stats-category-dropdown li { padding: 6px 15px; cursor: pointer; font-size: 13px; white-space: nowrap; } .stats-category-dropdown li:hover { background-color: #f0f0f0; } @media (max-width: 768px) { .popup-filters { flex-direction: column; align-items: stretch; } #player-switcher { margin: 10px 0; } .popup-header h3 { font-size: 1rem; } } `); // --- Page Router --- const params = new URLSearchParams(window.location.search); const p = params.get('p'); const sub = params.get('sub'); if (p === 'match' && sub === 'played') { initMatchPage(); } else if (p === 'players' || (p === 'player' && params.has('pid'))) { initPlayersPage(); } // --- Core Functions --- async function updateUserPlayerList() { const players = []; $('.playerContainer').each(function () { const nameLink = $(this).find('h2.subheader a.player_name, h2.subheader a .player_name, h2.subheader a[href*="pid="]').first(); const pid = $(this).find('span.player_id_span').first().text().trim() || new URL(nameLink.attr('href') || '', location.href).searchParams.get('pid'); const name = nameLink.text().trim(); if (pid && name) { const playerText = $(this).text(); const labeledAge = playerText.match(/(?:Age|Edad|Ya[^:]{0,5})\s*:\s*(\d{1,2})/i); const ageText = $(this).find('.dg_playerview_info table tr:first-child td:first-child').text(); let age = labeledAge ? parseInt(labeledAge[1], 10) : parseInt(ageText.replace(/[^0-9]/g, '')); if (isNaN(age)) { age = 0; } players.push({ pid: pid, name: name, age: age }); } }); if (players.length > 0) { await GM_setValue('myPlayerList', JSON.stringify(players)); } } async function populateSquadPlayers() { const myPlayerList = JSON.parse(await GM_getValue('myPlayerList', '[]')); if (myPlayerList.length > 0) { squadPlayersWithStats = [...myPlayerList]; squadPlayersWithStats.sort((a, b) => a.name.localeCompare(b.name, lang === 'tr' ? 'tr-TR' : undefined)); } else { squadPlayersWithStats = []; } } async function refreshUserPlayerListFromSource() { try { const response = await fetch('https://www.managerzone.com/?p=players'); if (!response.ok) { return false; } const html = await response.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const players = []; $(doc).find('.playerContainer').each(function () { const nameLink = $(this).find('h2.subheader a.player_name, h2.subheader a .player_name, h2.subheader a[href*="pid="]').first(); const pid = $(this).find('span.player_id_span').first().text().trim() || new URL(nameLink.attr('href') || '', location.href).searchParams.get('pid'); const name = nameLink.text().trim(); if (pid && name) { const playerText = $(this).text(); const labeledAge = playerText.match(/(?:Age|Edad|Ya[^:]{0,5})\s*:\s*(\d{1,2})/i); const ageText = $(this).find('.dg_playerview_info table tr:first-child td:first-child').text(); let age = labeledAge ? parseInt(labeledAge[1], 10) : parseInt(ageText.replace(/[^0-9]/g, '')); if (isNaN(age)) { age = 0; } players.push({ pid: pid, name: name, age: age }); } }); if (players.length > 0) { await GM_setValue('myPlayerList', JSON.stringify(players)); return true; } else { return false; } } catch (error) { return false; } } // =================================================================== // // --- MATCH PAGE FUNCTIONS --- // // =================================================================== // async function initMatchPage() { // Maçlar sayfasında takım ID'sini bulmak için sayfadaki ilk uygun linki kullan const firstTeamLink = $('#fixtures-results-list a[href*="&tid="]').first(); if (firstTeamLink.length > 0) { const urlParams_match = new URLSearchParams(firstTeamLink.attr('href').split('?')[1]); userTeamId = urlParams_match.get('tid'); } $('body').append('<div id="custom-tooltip"></div>').append(`<div id="mz-stat-loader"></div>`); setupTooltipListeners(); // --- MOBİL UYUMLULUK DÜZELTMESİ BURADA BAŞLIYOR --- // Butonları eklemek için daha güvenilir bir hedef element bulma mantığı let targetElement; // Önce masaüstü görünümünde kullanılan standart elementi arayalım. targetElement = $('#squad-search-toggle'); // Eğer bulunamazsa, mobil ve diğer görünümlerde de olan filtreleme formunu arayalım. if (targetElement.length === 0) { targetElement = $('select[name="selectType"]').closest('form'); } // Eğer form da bulunamazsa, filtrelerin bulunduğu genel kapsayıcıyı arayalım. // Bu, genellikle .baz.bazCenter yapısı içinde olur. if (targetElement.length === 0) { targetElement = $('select[name="selectType"]').closest('.baz.bazCenter > div'); } // Hiçbiri bulunamazsa, son çare olarak ana içerik alanının en üstüne ekleyelim. if (targetElement.length === 0) { targetElement = $('.mainContent').first(); if (targetElement.length > 0) { // Bu durumda 'after' yerine 'prepend' kullanarak en başa eklemek daha mantıklı. const actionsContainer = $('<div id="mz-stats-actions-container" style="margin-bottom: 10px;"></div>'); targetElement.prepend(actionsContainer); createScrapeButton(actionsContainer); createMatchPageActionButtons(actionsContainer); return; // İşlemi burada bitir. } } // --- MOBİL UYUMLULUK DÜZELTMESİ BURADA BİTİYOR --- if (targetElement.length > 0) { const actionsContainer = $('<div id="mz-stats-actions-container"></div>'); targetElement.after(actionsContainer); // Bulunan elementin sonrasına ekliyoruz. createScrapeButton(actionsContainer); createMatchPageActionButtons(actionsContainer); } } function createScrapeButton(container) { $(`<button id="scrapeStatsButton" class="mz-script-button">${i18n.autoScan}</button>`) .appendTo(container) .on('click', showMatchTypeSelectionPopup); } async function ensurePlayerListIsPopulated() { await populateSquadPlayers(); if (squadPlayersWithStats.length === 0) { const loader = $('#mz-stat-loader'); loader.text(i18n.loadingPlayerList).show(); const success = await refreshUserPlayerListFromSource(); loader.hide(); if (success) { await populateSquadPlayers(); } } if (squadPlayersWithStats.length === 0) { alert(i18n.noDataFound); return false; } return true; } async function updateMatchPageButtonStates() { const statsData = await GM_getValue('playerStatsRaw', '{}'); const hasData = Object.keys(JSON.parse(statsData)).length > 0; $('#viewPlayerStatsButton').prop('disabled', !hasData); $('#comparePlayersButtonMatchPage').prop('disabled', !hasData); $('#clearAllStatsButton').prop('disabled', !hasData); } function createMatchPageActionButtons(container) { container.find('#viewPlayerStatsButton, #comparePlayersButtonMatchPage, #clearAllStatsButton').remove(); $(`<button id="viewPlayerStatsButton" class="mz-script-button">${i18n.statistics}</button>`) .appendTo(container) .on('click', async () => { if (await ensurePlayerListIsPopulated()) showStatsPopup(squadPlayersWithStats[0].pid); }); $(`<button id="comparePlayersButtonMatchPage" class="mz-script-button">${i18n.compare}</button>`) .appendTo(container) .on('click', async () => { if (await ensurePlayerListIsPopulated()) showAllPlayersComparisonPopup(); }); $(`<button id="clearAllStatsButton" class="mz-script-button">${i18n.clearData}</button>`) .appendTo(container) .on('click', async () => { if (confirm(i18n.confirmClearData)) { await GM_deleteValue('playerStatsRaw'); await GM_deleteValue('processedMatchIds'); await updateMatchPageButtonStates(); } }); updateMatchPageButtonStates(); } async function showMatchTypeSelectionPopup() { $('#scrape-options-popup').remove(); const matchTypes = []; let matchTypeOptions = $('select[name="selectType"] optgroup:first option'); if (matchTypeOptions.length === 0) { try { const response = await fetch('https://www.managerzone.com/?p=match&sub=played', { credentials: 'include' }); if (response.ok) { const source = new DOMParser().parseFromString(await response.text(), 'text/html'); matchTypeOptions = $(source).find('select[name="selectType"] optgroup:first option'); } } catch (error) { console.warn('[MZone comparison] Could not load match types', error); } } matchTypeOptions.each(function () { matchTypes.push({ value: $(this).val(), text: $(this).text().trim() }); }); if (matchTypes.length === 0) { alert(i18n.noMatchTypes); return; } const optionsHTML = matchTypes.map(mt => `<li><label><input type="checkbox" class="match-type-checkbox" value="${mt.value}" data-text="${mt.text}" checked> ${mt.text}</label></li>`).join(''); // YENİ DÜĞMELERİN EKLENDİĞİ GÜNCELLENMİŞ HTML const popupHTML = ` <div id="scrape-options-popup"> <div class="popup-header"><h3>${i18n.autoScanOptions}</h3><span class="popup-close">×</span></div> <div class="popup-content-scrollable"> <p>${i18n.scanInfo}</p> <div class="scrape-options-controls"><a href="#" id="select-all-types">${i18n.selectAll}</a><a href="#" id="deselect-all-types">${i18n.deselectAll}</a></div> <ul id="scrape-options-list">${optionsHTML}</ul> <!-- YENİ EKLENEN BÖLÜM --> <div style="border-top: 1px solid #ccc; margin: 15px 0; padding-top: 15px; display: flex; flex-direction: column; gap: 10px;"> <button id="start-scrape-button">${i18n.scanSelectedCategories(matchTypes.length)}</button> <div style="display: flex; gap: 10px; justify-content: center;"> <button id="backup-json-btn" class="mz-script-button" style="background-color: #007bff; flex-grow: 1;">${i18n.backupData}</button> <button id="restore-json-btn" class="mz-script-button" style="background-color: #ffc107; color: #212529; flex-grow: 1;">${i18n.restoreData}</button> </div> </div> <!-- YENİ BÖLÜM SONU --> </div> </div>`; $('body').append(popupHTML); // Olay dinleyicileri (event listeners) $('.popup-close').on('click', () => $('#scrape-options-popup').remove()); $('#select-all-types').on('click', (e) => { e.preventDefault(); $('.match-type-checkbox').prop('checked', true).trigger('change'); }); $('#deselect-all-types').on('click', (e) => { e.preventDefault(); $('.match-type-checkbox').prop('checked', false).trigger('change'); }); $('.match-type-checkbox').on('change', () => { const count = $('.match-type-checkbox:checked').length; $('#start-scrape-button').text(i18n.scanSelectedCategories(count)).prop('disabled', count === 0); }); $('#start-scrape-button').on('click', function () { const selectedTypes = []; $('.match-type-checkbox:checked').each(function () { selectedTypes.push({ value: $(this).val(), text: $(this).data('text') }); }); if (selectedTypes.length > 0) { $('#scrape-options-popup').remove(); startAutomatedScrape(selectedTypes); } $(this).prop('disabled', true); }); // --- YENİ EKLENEN OLAY DİNLEYİCİLERİ --- $('#backup-json-btn').on('click', function () { handleJsonBackup($(this)); }); $('#restore-json-btn').on('click', function () { // Geri yükleme işlemi gizli bir dosya girişini tetikler. // Bu girişin var olduğundan emin olalım. if ($('#restore-file-input').length === 0) { $('<input type="file" id="restore-file-input" style="display: none;" accept=".json">') .appendTo('body') .on('change', processRestoreFile); } handleJsonRestore(); }); // --- YENİ DİNLEYİCİLER SONU --- } async function startAutomatedScrape(selectedTypes) { const loader = $('#mz-stat-loader'); loader.text(i18n.scanStarting).show(); const canonicalStatHeaders = [ null, 'Po', 'MP', 'G', 'A', 'S', 'G%', 'SOT', 'SOT%', 'OG', 'P', 'P_S', 'P_F', 'P%', 'PL', 'In', 'T_raw', 'T_S', 'T_F', 'T%', 'PosTime', 'Pos%', 'Dist', 'DistP', 'SpP', 'SpR', 'Cr', 'Fk', 'Pen', 'YC', 'RC', 'SV' ]; let newMatchesFound = false, playerStats = JSON.parse(await GM_getValue('playerStatsRaw', '{}')), processedMatchIds = new Set(JSON.parse(await GM_getValue('processedMatchIds', '[]'))), myPlayerList = JSON.parse(await GM_getValue('myPlayerList', '[]')), myPlayerPids = new Set(myPlayerList.map(p => p.pid)); let totalMatchesToScan = 0; let totalMatchesScanned = 0; loader.text(lang === 'tr' ? 'Taranacak toplam maç sayısı hesaplanıyor...' : 'Calculating total matches to scan...').show(); for (const category of selectedTypes) { try { const categoryUrl = `https://www.managerzone.com/?p=match&sub=played&selectType=${category.value}&selectTimeLimit=maximum`; const categoryResponse = await fetch(categoryUrl); const categoryHtml = await categoryResponse.text(); const categoryDoc = new DOMParser().parseFromString(categoryHtml, 'text/html'); const uniqueLinksMap = new Map(); $(categoryDoc).find('#fixtures-results-list a.gradientSunriseIcon[href*="sub=stats"]').each(function () { const href = $(this).attr('href'), mid = new URLSearchParams(href.split('?')[1]).get('mid'); if (mid && !uniqueLinksMap.has(mid)) uniqueLinksMap.set(mid, { mid: mid }); }); const newLinksToScrape = Array.from(uniqueLinksMap.values()).filter(linkInfo => !processedMatchIds.has(linkInfo.mid)); totalMatchesToScan += newLinksToScrape.length; } catch (e) { console.error(`Ön tarama hatası: ${category.text}`, e); } } for (const [categoryIndex, category] of selectedTypes.entries()) { loader.text(i18n.scanningCategory(category.text, categoryIndex + 1, selectedTypes.length)); try { const categoryUrl = `https://www.managerzone.com/?p=match&sub=played&selectType=${category.value}&selectTimeLimit=maximum`; const categoryResponse = await fetch(categoryUrl); const categoryHtml = await categoryResponse.text(); const categoryDoc = new DOMParser().parseFromString(categoryHtml, 'text/html'); const uniqueLinksMap = new Map(); $(categoryDoc).find('#fixtures-results-list a.gradientSunriseIcon[href*="sub=stats"]').each(function () { const href = $(this).attr('href'), mid = new URLSearchParams(href.split('?')[1]).get('mid'); if (mid && !uniqueLinksMap.has(mid)) uniqueLinksMap.set(mid, { href: href, mid: mid }); }); const newLinksToScrape = Array.from(uniqueLinksMap.values()).filter(linkInfo => !processedMatchIds.has(linkInfo.mid)); if (newLinksToScrape.length > 0) { newMatchesFound = true; for (const [linkIndex, linkInfo] of newLinksToScrape.entries()) { totalMatchesScanned++; loader.text(i18n.processingNewMatch( categoryIndex + 1, selectedTypes.length, category.text, linkIndex + 1, newLinksToScrape.length, totalMatchesScanned, totalMatchesToScan )); try { const matchUrl = new URL('https://www.managerzone.com/'); matchUrl.search = new URLSearchParams(linkInfo.href.split('?')[1]); const matchResponse = await fetch(matchUrl.toString()); const matchHtml = await matchResponse.text(); const matchDoc = new DOMParser().parseFromString(matchHtml, 'text/html'); let ageLimit = i18n.none; const categoryTextLower = category.text.toLowerCase(); if (categoryTextLower.includes('18')) ageLimit = i18n.under18; else if (categoryTextLower.includes('21')) ageLimit = i18n.under21; else if (categoryTextLower.includes('23')) ageLimit = i18n.under23; const ratingsMap = {}; // Özet tabloları bul (Class yapısı senin verdiğin HTML'e göre ayarlandı) $(matchDoc).find('table.hitlist.soccer.statsLite.marker.tablesorter tbody tr').each(function () { const playerLink = $(this).find('a[href*="pid="]'); if (playerLink.length) { const pid = new URLSearchParams(playerLink.attr('href').split('?')[1]).get('pid'); // Not sütunu (N) tablonun son sütunudur const ratingText = $(this).find('td').last().text().trim(); const rating = parseFloat(ratingText); if (pid && !isNaN(rating)) { ratingsMap[pid] = rating; } } }); $(matchDoc).find('.matchStats--detailed tbody tr').each(function () { const playerLink = $(this).find('a.player_link'); if (playerLink.length) { const pid = new URLSearchParams(playerLink.attr('href').split('?')[1]).get('pid'), name = playerLink.text().trim(); if (!myPlayerPids.has(pid)) { myPlayerList.push({ pid, name, age: 0 }); myPlayerPids.add(pid); } if (!playerStats[pid]) playerStats[pid] = { name: name, matches: [] }; playerStats[pid].name = name; const currentMatchStats = {}; $(this).find('td').each(function (colIndex) { const statName = canonicalStatHeaders[colIndex]; if (statName && !['Po', 'OG', 'Cr', 'Fk', 'P', 'P_S', 'P_F', 'T_raw', 'T_S', 'T_F', 'PosTime'].includes(statName)) { const rawValue = $(this).text().trim().replace('%', '').replace(',', '.').replace("'", ""); let numValue = parseFloat(rawValue); if (statName === 'T' && currentMatchStats['T%'] !== undefined) { } else if (!isNaN(numValue)) { currentMatchStats[statName] = numValue; } } }); if (ratingsMap[pid] !== undefined) { currentMatchStats['N'] = ratingsMap[pid]; } if (Object.keys(currentMatchStats).length > 0) { // DEĞİŞİKLİK BAŞLANGICI: Evrensel Filtreleme için `matchTypeValue` ekle playerStats[pid].matches.push({ matchId: linkInfo.mid, matchType: category.text, // Görüntüleme için matchTypeValue: category.value, // Filtreleme için ageLimit: ageLimit, stats: currentMatchStats }); // DEĞİŞİKLİK SONU } } }); processedMatchIds.add(linkInfo.mid); } catch (error) { console.error(i18n.errorFetchingMatch(linkInfo.mid), error); } await sleep(250); } } else { await sleep(500); } } catch (error) { loader.text(i18n.errorProcessingCategory(category.text)); await sleep(2000); } } await GM_setValue('playerStatsRaw', JSON.stringify(playerStats)); await GM_setValue('processedMatchIds', JSON.stringify(Array.from(processedMatchIds))); loader.text(i18n.scanFinishedVerifying); await refreshUserPlayerListFromSource(); await populateSquadPlayers(); loader.hide(); await updateMatchPageButtonStates(); $('#start-scrape-button').prop('disabled', false); if ($('#player-comparison-popup').length > 0) { await showAllPlayersComparisonPopup(); } } // ========================================================================= // // --- PLAYER PAGE FUNCTIONS --- // // ========================================================================= // const displayColumns = ['N', 'MP', 'G', 'A', 'S', 'G%', 'SOT', 'SOT%', 'P%', 'PL', 'In', 'T%', 'Pos%', 'Dist', 'DistP', 'SpP', 'SpR', 'YC', 'RC', 'SV']; const sumAvgColumns = ['G', 'A', 'YC', 'RC']; // DEĞİŞİKLİK BAŞLANGICI: Fonksiyonu `matchTypeValue` kullanacak şekilde güncelle function calculateAverages(playerData, filterTypeValue, filterAge) { if (!playerData || !playerData.matches) return null; const filteredMatches = playerData.matches.filter(match => (filterTypeValue === 'all' || match.matchTypeValue === filterTypeValue) && (filterAge === i18n.all || match.ageLimit === filterAge) ); // DEĞİŞİKLİK SONU if (filteredMatches.length === 0) return { name: playerData.name, matches: 0 }; const totalStats = {}, statCounts = {}; filteredMatches.forEach(match => { for (const key in match.stats) { totalStats[key] = (totalStats[key] || 0) + match.stats[key]; statCounts[key] = (statCounts[key] || 0) + 1; } }); const summary = {}; for (const key in totalStats) { if (sumAvgColumns.includes(key)) { summary[`${key}_sum`] = totalStats[key]; summary[`${key}_avg`] = (totalStats[key] / filteredMatches.length).toFixed(2); } else { summary[key] = (totalStats[key] / statCounts[key]).toFixed(2); } } return { name: playerData.name, matches: filteredMatches.length, ...summary }; } async function initPlayersPage() { // --- TAKIM ID'SİNİ ALMA KISMI (DOKUNMAYIN) --- const urlParams_players = new URLSearchParams(window.location.search); userTeamId = urlParams_players.get('tid'); if (!userTeamId) { const firstPlayerLink = $('h2.subheader a[href*="&tid="]').first(); if (firstPlayerLink.length > 0) { const hrefParams = new URLSearchParams(firstPlayerLink.attr('href').split('?')[1]); userTeamId = hrefParams.get('tid'); } } // --------------------------------------------------------------------- $('body').append('<div id="custom-tooltip"></div>'); if ($('#mz-stat-loader').length === 0) { $('body').append('<div id="mz-stat-loader"></div>'); } // --- EN ÖNEMLİ DÜZELTME BURADA --- // Sadece ana kadro sayfasındaysak (URL'de pid= yoksa) listeyi güncelle ve butonu ekle. if ($('.playerContainer').length > 0 && !urlParams_players.has('pid')) { await updateUserPlayerList(); $(`<button id="comparePlayersButton" class="mz-script-button">${i18n.compareAllPlayers}</button>`) .insertAfter('#squad-search-toggle') .on('click', showAllPlayersComparisonPopup); } // --------------------------------------------------------------------- await populateSquadPlayers(); await injectStatsIcons(); setupTooltipListeners(); $(document).on('click', '.stats-button', function (e) { e.preventDefault(); e.stopPropagation(); showStatsPopup($(this).data('pid')); }); $(document).on('click', '.stats-display-area', function (e) { e.stopPropagation(); $('.stats-category-dropdown').not($(this).siblings('.stats-category-dropdown')).hide(); $(this).siblings('.stats-category-dropdown').toggle(); }); $(document).on('click', '.stats-category-dropdown li', async function (e) { e.stopPropagation(); const selectedCategoryValue = $(this).data('value'); const selectedCategoryText = $(this).text(); const container = $(this).closest('.dynamic-stats-container'); const pid = container.data('pid'); const rawData = JSON.parse(await GM_getValue('playerStatsRaw', '{}')); const stats = getCategorySummaryStats(pid, selectedCategoryValue, rawData); const displayArea = container.find('.stats-display-area'); displayArea.find('.category-name').text(getShortCategoryName(selectedCategoryText)); displayArea.find('.stat-g').text(stats.G); displayArea.find('.stat-a').text(stats.A); displayArea.find('.stat-yc').text(stats.YC); displayArea.find('.stat-rc').text(stats.RC); $(this).parent().hide(); }); $(document).on('click', function () { $('.stats-category-dropdown').hide(); }); } async function injectStatsIcons() { await populateSquadPlayers(); const rawData = JSON.parse(await GM_getValue('playerStatsRaw', '{}')); if (Object.keys(rawData).length === 0) return; const myPlayerPidsWithStats = new Set(Object.keys(rawData)); const statsButtonHTML = `<button class="stats-button" style="padding: 2px 8px; font-size: 12px; margin-left: 10px; vertical-align: middle; background-color: #007bff; color: white; border: 1px solid #006fe6; border-radius: 12px; cursor: pointer;">${i18n.statistics}</button>`; const injectContent = (container) => { const pid = container.find('span.player_id_span').text().trim() || new URLSearchParams(window.location.search).get('pid'); if (!pid) return; const isSinglePlayerPage = container.is('div.player-title'); const headerTarget = isSinglePlayerPage ? container.find('h2').first() : container.find('h2.subheader a').first(); // Check parent for existing button to prevent duplicates // MZ'nin ID bulma mantığını bozmamak için linkin İÇİNE ekle if (myPlayerPidsWithStats.has(pid) && headerTarget.parent().find('.stats-button').length === 0) { headerTarget.append($(statsButtonHTML).data('pid', pid)); } const statsTd = container.find('td[colspan="2"][style*="padding-top: 12px;"]'); if (statsTd.length > 0 && statsTd.find('.dynamic-stats-container').length === 0 && myPlayerPidsWithStats.has(pid)) { const playerData = rawData[pid]; if (!playerData || !playerData.matches || playerData.matches.length === 0) return; // DEĞİŞİKLİK BAŞLANGICI: Kategorileri value ve text olarak ayır const availableCategories = new Map(); playerData.matches.forEach(m => { if (m.matchTypeValue && !availableCategories.has(m.matchTypeValue)) { availableCategories.set(m.matchTypeValue, m.matchType); } }); if (availableCategories.size === 0) return; let sortedCategories = Array.from(availableCategories.entries()).sort((a, b) => a[1].localeCompare(b[1])); sortedCategories.unshift(['all', i18n.all]); const defaultCategoryValue = 'all'; const defaultCategoryText = i18n.all; // DEĞİŞİKLİK SONU const defaultStats = getCategorySummaryStats(pid, defaultCategoryValue, rawData); // DEĞİŞİKLİK: `data-value` ve `data-category` ekle const dropdownItemsHTML = sortedCategories.map(([value, text]) => `<li data-value="${value}">${getShortCategoryName(text)}</li>`).join(''); const dynamicStatsHTML = ` <div class="dynamic-stats-container" data-pid="${pid}"> <div class="stats-display-area" title="İstatistik kategorisini değiştirmek için tıklayın"> <span class="category-name">${getShortCategoryName(defaultCategoryText)}</span><span style="font-weight: bold; font-size: 10px; margin-left: 2px;">▼</span> <div class="stats-values"> <span title="${i18n.statMappings['G']}"><img src="/nocache-936/img/soccer/goal.gif"> <strong class="stat-g">${defaultStats.G}</strong></span> <span title="${i18n.statMappings['A']}"><span class="assist-icon">👟</span> <strong class="stat-a">${defaultStats.A}</strong></span> <span title="${i18n.statMappings['YC']}"><img src="/nocache-936/img/card_yellow.gif"> <strong class="stat-yc">${defaultStats.YC}</strong></span> <span title="${i18n.statMappings['RC']}"><img src="/nocache-936/img/card_red.gif"> <strong class="stat-rc">${defaultStats.RC}</strong></span> </div> </div> <ul class="stats-category-dropdown">${dropdownItemsHTML}</ul> </div> `; statsTd.find('img[src*="goal.gif"], img[src*="card_yellow.gif"], img[src*="card_red.gif"], br').remove(); statsTd.contents().filter(function () { return this.nodeType === 3; }).remove(); statsTd.prepend(dynamicStatsHTML); } }; if ($('.playerContainer').length > 0) { $('.playerContainer').each(function () { injectContent($(this)); }); } else if (window.location.href.includes('p=player&pid=')) { injectContent($('div.player-title').closest('.mainContent')); } } // DEĞİŞİKLİK BAŞLANGICI: `matchTypeValue` ile filtreleme yapacak şekilde güncelle function getCategorySummaryStats(pid, filterTypeValue, rawData) { const playerData = rawData[pid]; if (!playerData || !playerData.matches) return { G: 0, A: 0, YC: 0, RC: 0 }; const filteredMatches = playerData.matches.filter(match => { if (!match.matchTypeValue) return false; if (filterTypeValue === 'all') return true; if (filterTypeValue === 'league') { // "Lig" için özel filtreleme return match.matchTypeValue.includes('league') || match.matchTypeValue.includes('series'); } return match.matchTypeValue === filterTypeValue; }); // DEĞİŞİKLİK SONU if (filteredMatches.length === 0) return { G: 0, A: 0, YC: 0, RC: 0 }; return filteredMatches.reduce((acc, match) => { acc.G += match.stats.G || 0; acc.A += match.stats.A || 0; acc.YC += match.stats.YC || 0; acc.RC += match.stats.RC || 0; return acc; }, { G: 0, A: 0, YC: 0, RC: 0 }); } async function showStatsPopup(pid) { $('#player-stats-popup').remove(); await populateSquadPlayers(); if (squadPlayersWithStats.length === 0) { alert(i18n.noPlayersToShow); return; } const rawData = JSON.parse(await GM_getValue('playerStatsRaw', '{}')); const playerOptionsHTML = squadPlayersWithStats.map(player => `<option value="${player.pid}" ${player.pid === pid ? 'selected' : ''}>${player.name}</option>`).join(''); const popupHTML = ` <div id="player-stats-popup"> <div class="popup-header"> <h3 id="popup-title"></h3><span class="popup-close">×</span> </div> <div class="popup-content-scrollable"> <div style="display: flex; align-items: center;"> <select id="player-switcher">${playerOptionsHTML}</select> </div> <div id="popup-category-selector" class="popup-filters" style="flex-direction: column; align-items: stretch; border: 1px solid #ccc; border-radius: 5px; padding: 5px; cursor: pointer; position: relative; margin-top: 15px;"> <div id="category-display-wrapper" style="display: flex; justify-content: space-between; align-items: center;"> <span id="current-category-name" style="font-weight: bold; color: #00529B;"></span> <div id="current-category-stats" style="display: flex; gap: 10px; align-items: center; font-size: 12px;"></div> <span id="category-dropdown-trigger" style="font-weight: bold; padding: 0 5px;">▼</span> </div> <ul id="category-dropdown-list" style="display: none; position: absolute; top: 100%; left: -1px; right: -1px; background: white; border: 1px solid #ccc; list-style: none; padding: 0; margin: 5px 0 0; z-index: 10001; max-height: 200px; overflow-y: auto; border-radius: 0 0 5px 5px;"> </ul> </div> <table id="popup-stats-table" style="margin-top:15px;"><thead><tr><th>${i18n.statistics}</th><th>Value</th></tr></thead><tbody></tbody></table> </div> </div>`; $('body').append(popupHTML); const updatePopupForPlayer = (currentPid) => { const playerData = rawData[currentPid]; const availableCategories = new Map(); if (playerData && playerData.matches) { playerData.matches.forEach(m => { if (m.matchTypeValue) availableCategories.set(m.matchTypeValue, m.matchType) }); } const sortedCategories = Array.from(availableCategories.entries()).sort((a, b) => a[1].localeCompare(b[1])); if (sortedCategories.length > 0) { sortedCategories.unshift(['all', i18n.all]); const categoryDropdown = $('#category-dropdown-list').empty(); sortedCategories.forEach(([value, text]) => { categoryDropdown.append(`<li data-value="${value}" style="padding: 8px 12px; cursor: pointer;">${getShortCategoryName(text)}</li>`); }); $('#popup-category-selector').show(); const defaultCategoryValue = 'all'; $('#popup-category-selector').attr('data-selected-category', defaultCategoryValue); $('#current-category-name').text(getShortCategoryName(i18n.all)); updateCategoryStatsDisplay(currentPid, defaultCategoryValue, rawData); updateStatsPopupTable(currentPid, defaultCategoryValue); } else { $('#popup-category-selector').hide(); updateStatsPopupTable(currentPid, 'all'); } }; const updateCategoryStatsDisplay = (currentPid, categoryValue, rawData) => { const stats = getCategorySummaryStats(currentPid, categoryValue, rawData); $('#current-category-stats').html(` <span title="${i18n.statMappings['G']}"><img src="/nocache-936/img/soccer/goal.gif" style="width:10px; vertical-align: middle;"> ${stats.G}</span> <span title="${i18n.statMappings['A']}"><span class="assist-icon" style="vertical-align: middle;">👟</span> ${stats.A}</span> <span title="${i18n.statMappings['YC']}"><img src="/nocache-936/img/card_yellow.gif" style="width:8px; vertical-align: middle;"> ${stats.YC}</span> <span title="${i18n.statMappings['RC']}"><img src="/nocache-936/img/card_red.gif" style="width:8px; vertical-align: middle;"> ${stats.RC}</span> `); }; $('#player-switcher').on('change', function () { updatePopupForPlayer($(this).val()); }); $('#popup-category-selector').on('click', (e) => { e.stopPropagation(); $('#category-dropdown-list').slideToggle(200); }); $('#category-dropdown-list').on('mouseenter', 'li', function () { $(this).css('background-color', '#f0f0f0'); }); $('#category-dropdown-list').on('mouseleave', 'li', function () { $(this).css('background-color', 'white'); }); $('#category-dropdown-list').on('click', 'li', function (e) { e.stopPropagation(); const selectedCategoryValue = $(this).data('value'); const selectedCategoryText = $(this).text(); $('#popup-category-selector').attr('data-selected-category', selectedCategoryValue); $('#current-category-name').text(getShortCategoryName(selectedCategoryText)); const currentPid = $('#player-switcher').val(); updateCategoryStatsDisplay(currentPid, selectedCategoryValue, rawData); updateStatsPopupTable(currentPid, selectedCategoryValue); $('#category-dropdown-list').hide(); }); $(document).on('click.statsPopup', function (e) { if (!$('#popup-category-selector').is(e.target) && $('#popup-category-selector').has(e.target).length === 0) { $('#category-dropdown-list').slideUp(200); } }); $('#player-stats-popup .popup-close').on('click', () => { $('#player-stats-popup').remove(); $(document).off('click.statsPopup'); }); updatePopupForPlayer(pid); } async function updateStatsPopupTable(pid, filterTypeValue) { $('#player-stats-popup').data('pid', pid); const rawData = JSON.parse(await GM_getValue('playerStatsRaw', '{}')); let data = calculateAverages(rawData[pid], filterTypeValue, i18n.all); const tableBody = $('#popup-stats-table tbody').empty(); if (!data) { const playerInfo = squadPlayersWithStats.find(p => p.pid === pid); data = { name: playerInfo ? playerInfo.name : 'Unknown Player', matches: 0 }; } const matchesText = data.matches > 0 ? i18n.avgStatsForMatches(data.matches) : ''; $('#popup-title').html(`${data.name} <br><small style="color: #6c757d;">${matchesText}</small>`); if (data.matches > 0) { displayColumns.forEach(key => { const displayName = i18n.statMappings[key] || key; if (sumAvgColumns.includes(key)) { const sum = data[`${key}_sum`] || 0, avg = data[`${key}_avg`] || '0.00'; tableBody.append(`<tr><td>${displayName} (Tot/Avg)</td><td><strong>${sum} / ${avg}</strong></td></tr>`); } else if (data[key] !== undefined) { const suffix = key.includes('%') ? ' %' : ''; tableBody.append(`<tr><td>${displayName}</td><td><strong>${data[key]}${suffix}</strong></td></tr>`); } }); } else { tableBody.append(`<tr><td colspan="2">${i18n.noMatchesForCriteria}</td></tr>`); } } async function showAllPlayersComparisonPopup() { $('#player-comparison-popup').remove(); if (window.location.href.includes('?p=players')) { await updateUserPlayerList(); } await populateSquadPlayers(); if (squadPlayersWithStats.length === 0) { alert(i18n.noDataFound); return; } const rawData = JSON.parse(await GM_getValue('playerStatsRaw', '{}')); const allMatches = Object.values(rawData).flatMap(p => p.matches || []); const matchTypes = new Map([['all', i18n.all]]); allMatches.forEach(m => { if (m.matchTypeValue && !matchTypes.has(m.matchTypeValue)) { matchTypes.set(m.matchTypeValue, m.matchType); } }); const sortedMatchTypes = Array.from(matchTypes.entries()).sort((a, b) => a[0] === 'all' ? -1 : (b[0] === 'all' ? 1 : a[1].localeCompare(b[1]))); const matchTypeOptions = sortedMatchTypes.map(([value, text]) => `<option value="${value}">${text}</option>`).join(''); const ageLimits = [i18n.all, i18n.none, i18n.under18, i18n.under21, i18n.under23]; let tableHeaderHTML = `<th data-sort-type="text" data-tooltip="${i18n.player}">${i18n.player}</th><th data-sort-type="numeric" data-tooltip="${i18n.age}">${i18n.age}</th><th data-sort-type="numeric" data-tooltip="${i18n.matches}">${i18n.matches}</th>${displayColumns.map(col => `<th data-sort-type="numeric" data-tooltip="${i18n.statMappings[col] || col}">${col}</th>`).join('')}`; // --- KOŞULLU HTML OLUŞTURMA (MOBİL / MASAÜSTÜ AYRIMI) --- let ageFilterHTML = ''; if ($.fn.slider) { // MASAÜSTÜ: Slider'lı filtre ageFilterHTML = ` <div id="age-slider-filter-container"> <label for="agea_slider_input">${i18n.age}:</label> <input type="text" id="agea_slider_input" class="age-slider-input"> <div id="age-slider-range" class="ui-slider ui-widget-content ui-corner-all"></div> <input type="text" id="ageb_slider_input" class="age-slider-input"> </div>`; } else { // MOBİL: Basit input'lu ve "Uygula" butonlu filtre const applyText = lang === 'tr' ? "Uygula" : "Apply"; ageFilterHTML = ` <div id="age-filter-container-mobile" style="display:flex; align-items:center; gap: 5px; flex-wrap: wrap;"> <label for="min-age-input" style="font-weight:bold; margin-bottom:0;">${i18n.age}:</label> <input type="text" inputmode="numeric" pattern="[0-9]*" id="min-age-input" class="age-slider-input" placeholder="Min" value="16" maxlength="2" style="width: 55px;"> <span>-</span> <input type="text" inputmode="numeric" pattern="[0-9]*" id="max-age-input" class="age-slider-input" placeholder="Max" value="38" maxlength="2" style="width: 55px;"> <button id="apply-age-filter-btn" class="mz-script-button" style="padding: 4px 8px; font-size: 12px; background-color: #007bff; margin-left: 5px;">${applyText}</button> </div>`; } const popupHTML = ` <div id="player-comparison-popup"> <div class="popup-header"><h3>${i18n.playerComparison}</h3><span class="popup-close">×</span></div> <div class="popup-content-scrollable"> <div class="popup-filters"> <select id="compare-filter-type">${matchTypeOptions}</select> <select id="compare-filter-age">${ageLimits.map(opt => `<option>${opt}</option>`).join('')}</select> ${ageFilterHTML} <button id="export-to-excel-btn" class="mz-script-button" style="background-color: #28a745; margin-left: auto;">${i18n.exportToExcel}</button> </div> <div class="table-responsive-wrapper"> <table id="comparison-table"><thead><tr>${tableHeaderHTML}</tr></thead><tbody></tbody></table> </div> </div> </div>`; $('body').append(popupHTML); if ($.fn.slider) { // --- MASAÜSTÜ ÖZEL KODU --- const minAgeInput = $("#agea_slider_input"); const maxAgeInput = $("#ageb_slider_input"); const ageSlider = $("#age-slider-range"); let debounceTimer; ageSlider.slider({ range: true, min: 16, max: 38, values: [16, 38], slide: (event, ui) => { minAgeInput.val(ui.values[0]); maxAgeInput.val(ui.values[1]); }, change: (event, ui) => { if (event.originalEvent) { updateComparisonTable(); } } }); minAgeInput.val(ageSlider.slider("values", 0)); maxAgeInput.val(ageSlider.slider("values", 1)); minAgeInput.add(maxAgeInput).on("input", function () { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { let min = parseInt(minAgeInput.val(), 10), max = parseInt(maxAgeInput.val(), 10); const currentValues = ageSlider.slider("values"); const sliderMin = isNaN(min) ? currentValues[0] : Math.max(16, min); const sliderMax = isNaN(max) ? currentValues[1] : Math.min(38, max); if (sliderMin <= sliderMax) ageSlider.slider("values", [sliderMin, sliderMax]); updateComparisonTable(); }, 400); }).on("blur", function () { let min = parseInt(minAgeInput.val(), 10) || 16, max = parseInt(maxAgeInput.val(), 10) || 38; if (min > max) [min, max] = [max, min]; min = Math.max(16, min); max = Math.min(38, max); minAgeInput.val(min); maxAgeInput.val(max); }); } else { // --- MOBİL ÖZEL KODU --- $('#apply-age-filter-btn').on('click', function () { updateComparisonTable(); }); } updateComparisonTable(); $(document).off('click', '#comparison-table td:first-child').on('click', '#comparison-table td:first-child', function () { const pid = $(this).closest('tr').data('pid'); if (pid && userTeamId) GM_openInTab(`https://www.managerzone.com/?p=players&pid=${pid}&tid=${userTeamId}`, false); }); $('#player-comparison-popup .popup-close').on('click', () => $('#player-comparison-popup').remove()); $('#player-comparison-popup .popup-filters select').on('change', updateComparisonTable); $('#export-to-excel-btn').on('click', handleExcelExport); } async function handleExcelExport() { const button = $('#export-to-excel-btn'); const originalText = button.text(); button.text(i18n.exporting).prop('disabled', true); try { const wb = XLSX.utils.book_new(); const rawData = JSON.parse(await GM_getValue('playerStatsRaw', '{}')); const filterAge = $('#compare-filter-age').val(); const matchTypes = new Map(); $('#compare-filter-type option').each(function () { matchTypes.set($(this).val(), $(this).text()); }); for (const [filterTypeValue, filterTypeText] of matchTypes.entries()) { const sheetData = await generateSheetData(rawData, filterTypeValue, filterAge); if (sheetData.length > 0) { const ws = XLSX.utils.json_to_sheet(sheetData); const sheetName = filterTypeText.replace(/[\/\\?*\[\]]/g, '').substring(0, 31); XLSX.utils.book_append_sheet(wb, ws, sheetName); } } if (wb.SheetNames.length === 0) { alert(i18n.noDataFound); return; } XLSX.writeFile(wb, 'ManagerZone_Player_Comparison.xlsx'); } catch (error) { console.error("Excel export failed:", error); alert(i18n.exportError); } finally { button.text(originalText).prop('disabled', false); } } // Yedekten geri yükleme işlemini başlatan fonksiyon async function handleJsonBackup(button) { const originalText = button.text(); button.text(i18n.backupInProgress).prop('disabled', true); try { // <<< DEĞİŞİKLİK BAŞLADI >>> // Sadece istatistikleri değil, işlenmiş maç ID'lerini de alıyoruz. const rawData = await GM_getValue('playerStatsRaw', '{}'); const processedIds = await GM_getValue('processedMatchIds', '[]'); // İki veriyi de tek bir nesnede birleştiriyoruz. // Bu, yedeği daha eksiksiz ve güvenilir hale getirir. const backupObject = { stats: JSON.parse(rawData), processedIds: JSON.parse(processedIds) }; // Bu birleştirilmiş nesneyi JSON formatına çeviriyoruz. const dataStr = JSON.stringify(backupObject, null, 2); // <<< DEĞİŞİKLİK SONA ERDİ >>> const blob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `mz_player_stats_backup_${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (error) { console.error("JSON backup failed:", error); alert("Yedekleme sırasında bir hata oluştu."); } finally { button.text(originalText).prop('disabled', false); } } // Yedekten geri yükleme işlemini başlatan fonksiyon function handleJsonRestore() { $('#restore-file-input').click(); // Gizli dosya girişini tetikle } // Kullanıcı bir dosya seçtiğinde çalışan fonksiyon async function processRestoreFile(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async function (e) { try { const content = e.target.result; const jsonData = JSON.parse(content); // <<< DEĞİŞİKLİK BAŞLADI >>> // YENİ FORMAT KONTROLÜ: Yedek dosyası hem 'stats' hem de 'processedIds' içeriyor mu? if (typeof jsonData === 'object' && jsonData.stats && Array.isArray(jsonData.processedIds)) { // Yeni format algılandı. Her iki veriyi de geri yüklüyoruz. await GM_setValue('playerStatsRaw', JSON.stringify(jsonData.stats)); await GM_setValue('processedMatchIds', JSON.stringify(jsonData.processedIds)); // EN ÖNEMLİ DÜZELTME alert(i18n.restoreSuccess); updateMatchPageButtonStates(); } else if (typeof jsonData === 'object' && Object.keys(jsonData).every(key => !isNaN(parseInt(key)))) { // GERİ UYUMLULUK: Sadece istatistik içeren eski formatta bir yedek algılandı. console.warn("Eski formatta yedek dosyası algılandı. İşlenmiş Maç ID'leri istatistiklerden yeniden oluşturuluyor."); await GM_setValue('playerStatsRaw', JSON.stringify(jsonData)); // İstatistik verilerinden yola çıkarak 'processedMatchIds' listesini yeniden oluşturuyoruz. const allIds = new Set(); Object.values(jsonData).forEach(player => { if (player.matches && Array.isArray(player.matches)) { player.matches.forEach(match => { if (match.matchId) allIds.add(match.matchId); }); } }); await GM_setValue('processedMatchIds', JSON.stringify(Array.from(allIds))); alert(i18n.restoreSuccess + " (Eski yedek formatı algılandı, eksik veriler yeniden oluşturuldu.)"); updateMatchPageButtonStates(); } else { // Dosya formatı tanınmıyorsa hata ver. throw new Error("Invalid data structure"); } // <<< DEĞİŞİKLİK SONA ERDİ >>> } catch (error) { console.error("Failed to process restore file:", error); alert(i18n.restoreError); } }; reader.readAsText(file); // Aynı dosyayı tekrar seçebilmek için input'u sıfırla event.target.value = ''; } async function generateSheetData(rawData, filterTypeValue, filterAge) { const dataForSheet = []; for (const player of squadPlayersWithStats) { const data = calculateAverages(rawData[player.pid], filterTypeValue, filterAge); if (data && data.matches > 0) { const playerRow = {}; playerRow[i18n.player] = player.name; playerRow[i18n.age] = player.age > 0 ? player.age : '?'; playerRow[i18n.matches] = data.matches; displayColumns.forEach(col => { const displayName = i18n.statMappings[col] || col; if (sumAvgColumns.includes(col)) { const sum = data[`${col}_sum`] || 0; const avg = data[`${col}_avg`] || '0.00'; playerRow[`${displayName} ${i18n.totalSuffix}`] = sum; playerRow[`${displayName} ${i18n.avgSuffix}`] = parseFloat(avg); } else { const statValue = data[col] || '0.00'; playerRow[displayName] = parseFloat(statValue); } }); dataForSheet.push(playerRow); } } return dataForSheet; } // --- KESİN ÇALIŞAN SIRALAMA FONKSİYONU (VERSİYON 3) --- // // =================================================================== // /** * Tabloyu, her satırdaki "yeşil" (iyi) ve "kırmızı" (kötü) hücrelere göre * bir puan hesaplayarak başlangıçta sıralar. En yüksek puana sahip (en yeşil) * oyuncu en üste gelir. * @param {jQuery} table - Sıralanacak tablo nesnesi. */ function initialSortByGreenness(table) { const rows = table.find('tbody tr').get(); rows.sort((a, b) => { let scoreA = 0; let scoreB = 0; // A satırı için puanı, önceden kaydettiğimiz 'data-hue' üzerinden hesapla $(a).find('td[data-hue]').each(function () { const hue = parseFloat($(this).attr('data-hue')) || 0; // Puanlama mantığı: Yeşil (120) pozitif, Kırmızı (0) negatif puan verir. scoreA += (hue - 60); }); // B satırı için puanı, önceden kaydettiğimiz 'data-hue' üzerinden hesapla $(b).find('td[data-hue]').each(function () { const hue = parseFloat($(this).attr('data-hue')) || 0; scoreB += (hue - 60); }); // Puanı yüksek olan (daha çok yeşil olan) üste gelecek şekilde sırala return scoreB - scoreA; }); // Satırları yeni sıralamalarına göre tabloya yeniden ekle $.each(rows, (idx, row) => table.children('tbody').append(row)); } async function updateComparisonTable() { // YENİ EKLENDİ: Emeklilik verilerini tarayıcı hafızasından çekiyoruz. const retirementDataJSON = await GM_getValue(RETIREMENT_DATA_KEY, '{}'); const retirementData = JSON.parse(retirementDataJSON); const minAge = parseInt($('#agea_slider_input').val() || $('#min-age-input').val(), 10) || 16; const maxAge = parseInt($('#ageb_slider_input').val() || $('#max-age-input').val(), 10) || 38; const filterTypeValue = $('#compare-filter-type').val(), filterAge = $('#compare-filter-age').val(), rawData = JSON.parse(await GM_getValue('playerStatsRaw', '{}')), tableBody = $('#comparison-table tbody').empty(); if (!userTeamId) { console.error("Takım ID'si bulunamadı! Linkler oluşturulamıyor."); tableBody.append('<tr><td colspan="100%">Hata: Takım ID alınamadı. Lütfen sayfayı yenileyip tekrar deneyin.</td></tr>'); return; } for (const player of squadPlayersWithStats) { if (player.age < minAge || player.age > maxAge) { continue; } const data = calculateAverages(rawData[player.pid], filterTypeValue, filterAge); if (data && data.matches > 0) { const currentAge = player.age, displayAge = currentAge > 0 ? currentAge : '?'; // YENİ EKLENDİ: Oyuncunun emeklilik durumuna göre ikonları oluşturacak bölüm. let retirementIconsHTML = ''; if (retirementData[player.pid]) { // Eğer oyuncu emeklilik listesindeyse, ikon HTML'ini hazırlıyoruz. // Bu kod, diğer modüldekiyle tamamen aynıdır, böylece görünüm tutarlı olur. retirementIconsHTML = `<span class="retirement-indicator-container" style="display: inline-flex; align-items: center; gap: 4px; margin-right: 6px; vertical-align: middle;">`; // Uyarı üçgeni retirementIconsHTML += `<i class="fa fa-exclamation-triangle" title="${i18n.retireWarning}" style="font-size: 14px; color: rgb(230, 0, 0); cursor: help; line-height: 1;"></i>`; // Eğer uzatılabiliyorsa, kalp ikonu if (retirementData[player.pid].extendable) { retirementIconsHTML += `<span class="player_icon_placeholder view_extend_career" title="${i18n.canExtendRetirement}" style="cursor: help; margin: 0px; padding: 0px; display: inline-flex; align-items: center;"> <span class="player_icon_wrapper" style="display: inline-flex; align-items: center;"> <i class="fa-duotone fa-heart-pulse extend-career-icon" style="font-size: 14px;"></i> </span> </span>`; } retirementIconsHTML += `</span>`; } // Oyuncu ismini içeren hücreyi oluştururken, hazırladığımız ikon HTML'ini başına ekliyoruz. let row = `<tr data-pid="${player.pid}"> <td style="color: #00529B; cursor: pointer; display: flex; justify-content: space-between; align-items: center;"> <span style="text-decoration: underline;">${player.name}</span> ${retirementIconsHTML} </td> <td data-sort="${currentAge}">${displayAge}</td> <td data-sort="${data.matches}">${data.matches}</td>`; displayColumns.forEach(col => { if (sumAvgColumns.includes(col)) { const sum = data[`${col}_sum`] || 0, avg = data[`${col}_avg`] || '0.00'; row += `<td data-sort-sum="${sum}" data-sort-avg="${avg}">${sum} / ${avg}</td>`; } else { const statValue = data[col] || '0.00', suffix = col.includes('%') ? ' %' : ''; row += `<td data-sort="${parseFloat(statValue)}">${statValue}${suffix}</td>`; } }); row += '</tr>'; tableBody.append(row); } } const hasRows = tableBody.children('tr').length > 0; $('#export-to-excel-btn').prop('disabled', !hasRows); if (!hasRows) { const needsInitialScan = Object.keys(rawData).length === 0; const message = needsInitialScan ? i18n.statsNeedScan : i18n.noMatchesForCriteria; const action = needsInitialScan ? `<button type="button" id="mz-comparison-scan-now" class="mz-script-button" style="background-color:#007bff;margin-top:8px">${i18n.scanMatchStats}</button>` : ''; tableBody.append(`<tr class="mz-comparison-empty"><td colspan="100%"><div>${message}</div>${action}</td></tr>`); $('#mz-comparison-scan-now').off('click').on('click', () => showMatchTypeSelectionPopup()); } makeTableSortable($('#comparison-table')); applyCellColoring($('#comparison-table')); initialSortByGreenness($('#comparison-table')); } function makeTableSortable(table) { table.find('th[data-sort-type]').off('click').on('click', function () { const th = $(this), index = th.index(), headerText = th.text().trim(), isSpecialSort = sumAvgColumns.includes(headerText); table.find('th').not(th).removeClass('sort-asc sort-desc sort-sum-asc sort-sum-desc sort-avg-asc sort-avg-desc').removeAttr('data-sort-state'); let sortKey, direction, sortAttribute; if (isSpecialSort) { const states = ['sum-desc', 'sum-asc', 'avg-desc', 'avg-asc'], currentState = th.attr('data-sort-state') || '', nextIndex = (states.indexOf(currentState) + 1) % states.length, newState = states[nextIndex]; th.attr('data-sort-state', newState).removeClass('sort-asc sort-desc sort-sum-asc sort-sum-desc sort-avg-asc sort-avg-desc').addClass(`sort-${newState}`); [sortKey, direction] = newState.split('-'); sortAttribute = `data-sort-${sortKey}`; } else { direction = th.hasClass('sort-asc') ? 'desc' : 'asc'; th.removeClass('sort-asc sort-desc').addClass(`sort-${direction}`); sortAttribute = 'data-sort'; th.removeAttr('data-sort-state'); } const rows = table.find('tbody tr').get(); rows.sort((a, b) => { let valA, valB; const tdA = $(a).find('td').eq(index), tdB = $(b).find('td').eq(index); if (isSpecialSort) { valA = parseFloat(tdA.attr(sortAttribute)) || 0; valB = parseFloat(tdB.attr(sortAttribute)) || 0; } else { valA = tdA.attr(sortAttribute) || 0; valB = tdB.attr(sortAttribute) || 0; if (isNaN(parseFloat(valA)) || isNaN(parseFloat(valB))) return direction === 'asc' ? valA.localeCompare(valB, lang === 'tr' ? 'tr-TR' : undefined) : valB.localeCompare(valA, lang === 'tr' ? 'tr-TR' : undefined); valA = parseFloat(valA); valB = parseFloat(valB); } return direction === 'asc' ? valA - valB : valB - valA; }); $.each(rows, (idx, row) => table.children('tbody').append(row)); }); } function applyCellColoring(table) { const headers = table.find('thead th').map(function () { return $(this).text(); }).get(); const goodStats = ['MP', 'G', 'A', 'S', 'G%', 'SOT', 'SOT%', 'P%', 'PL', 'In', 'T', 'T%', 'Pos%', 'Dist', 'DistP', 'SpR', 'SV', 'SpP']; const badStats = ['YC', 'RC']; headers.forEach((header, index) => { if (goodStats.includes(header) || badStats.includes(header)) { let values = []; table.find(`tbody tr td:nth-child(${index + 1})`).each(function () { let sortVal = sumAvgColumns.includes(header) ? $(this).attr('data-sort-avg') : $(this).attr('data-sort'); values.push(parseFloat(sortVal) || 0); }); const min = Math.min(...values), max = Math.max(...values); if (min === max) return; table.find(`tbody tr td:nth-child(${index + 1})`).each(function () { let sortVal = sumAvgColumns.includes(header) ? $(this).attr('data-sort-avg') : $(this).attr('data-sort'); const value = parseFloat(sortVal) || 0, normalized = max === min ? 0.5 : (value - min) / (max - min); let hue = badStats.includes(header) ? 120 - (normalized * 120) : normalized * 120; // DEĞİŞEN SATIR BURASI: .attr('data-hue', hue) eklendi $(this).css('background-color', `hsla(${hue}, 70%, 75%, 0.6)`).addClass('colored-cell').attr('data-hue', hue); }); } }); } function setupTooltipListeners() { const tooltip = $('#custom-tooltip'); let currentTarget = null; $(document).on('click', '[data-tooltip]', function (e) { // e.stopPropagation(); // BU SATIR SİLİNDİ: Orijinal site butonlarını (paketler vb.) engellememek için. const target = $(this); const tooltipText = target.attr('data-tooltip'); if (currentTarget && currentTarget[0] === target[0]) { tooltip.hide(); currentTarget = null; return; } currentTarget = target; if (tooltipText) { tooltip.html(tooltipText).show(); const targetOffset = target.offset(), targetHeight = target.outerHeight(), targetWidth = target.outerWidth(), tooltipHeight = tooltip.outerHeight(), tooltipWidth = tooltip.outerWidth(); let top = targetOffset.top - tooltipHeight - 10; let left = targetOffset.left + (targetWidth / 2) - (tooltipWidth / 2); if (top < 0) top = targetOffset.top + targetHeight + 10; if (left < 0) left = 5; if (left + tooltipWidth > $(window).width()) left = $(window).width() - tooltipWidth - 5; tooltip.css({ top: top, left: left }); } }); $(document).on('click', function (e) { if (currentTarget && !$(e.target).is(currentTarget)) { tooltip.hide(); currentTarget = null; } }); $(document).on('mouseenter', '[data-tooltip]', function () { if ('ontouchstart' in window) return; // Don't show on hover for touch devices const target = $(this); const tooltipText = target.attr('data-tooltip'); if (tooltipText) { tooltip.html(tooltipText).show(); const targetOffset = target.offset(), targetHeight = target.outerHeight(), targetWidth = target.outerWidth(), tooltipHeight = tooltip.outerHeight(), tooltipWidth = tooltip.outerWidth(); let top = targetOffset.top - tooltipHeight - 10; let left = targetOffset.left + (targetWidth / 2) - (tooltipWidth / 2); if (top < 0) top = targetOffset.top + targetHeight + 10; if (left < 0) left = 5; if (left + tooltipWidth > $(window).width()) left = $(window).width() - tooltipWidth - 5; tooltip.css({ top: top, left: left }); } }).on('mouseleave', '[data-tooltip]', function () { if ('ontouchstart' in window) return; // Don't hide on leave for touch devices if it was opened by click if (!currentTarget) { // Only hide if it wasn't "click-locked" tooltip.hide(); } }); } } /**************************************************************************************** * * * BÖLÜM 4: PLAY-OFF/PLAY-OUT TAHMİNCİSİ (Evrensel) * * * ****************************************************************************************/ function initializePlayoffPredictorScript() { let isJobRunning = false; function parseLeagueName(name) { const lowerName = name.toLowerCase().trim(); const match = lowerName.match(/div(\d+)\.(\d+)/); if (match) { return { level: parseInt(match[1], 10), index: parseInt(match[2], 10), isTopLeague: false }; } else if (lowerName && !lowerName.startsWith('div')) { return { level: 0, index: 1, isTopLeague: true }; } return null; } function generateRules(level, index, currentLeagueName, isTopLeague) { const generatedRules = []; if (!isTopLeague) { const parentLevel = level - 1; const parentIndex = Math.floor((index - 1) / 3) + 1; const leagueSelect = getLeagueSelect(); let parentLeagueName = `div${parentLevel}.${parentIndex}`; if (parentLevel === 0) { for (const option of leagueSelect.options) { const leagueInfo = parseLeagueName(option.text); if (leagueInfo && leagueInfo.isTopLeague) { parentLeagueName = option.text; break; } } } const firstSiblingIndex = (parentIndex - 1) * 3 + 1; const childLeague1 = `div${level}.${firstSiblingIndex}`; const childLeague2 = `div${level}.${firstSiblingIndex + 1}`; const childLeague3 = `div${level}.${firstSiblingIndex + 2}`; const groupBaseNum = parentIndex * 2; generatedRules.push({ name: `Play-Off Grup ${groupBaseNum - 1} (Yükselme)`, type: 'playoff', teams: [ { league: parentLeagueName, rank: 8 }, { league: childLeague1, rank: 2 }, { league: childLeague2, rank: 3 }, { league: childLeague3, rank: 3 } ] }); generatedRules.push({ name: `Play-Off Grup ${groupBaseNum} (Yükselme)`, type: 'playoff', teams: [ { league: parentLeagueName, rank: 9 }, { league: childLeague1, rank: 3 }, { league: childLeague2, rank: 2 }, { league: childLeague3, rank: 2 } ] }); } if (level < 6) { const nextLevel = level + 1; const childBaseIndex = (index - 1) * 3; const childLeague1 = `div${nextLevel}.${childBaseIndex + 1}`; const childLeague2 = `div${nextLevel}.${childBaseIndex + 2}`; const childLeague3 = `div${nextLevel}.${childBaseIndex + 3}`; const groupNameSuffix = isTopLeague ? "(Yükselme/Düşme)" : "(Düşme)"; const groupBaseIndex = isTopLeague ? 0 : (((3 ** (level - 1) - 1) / 2) + index - 1); const groupNum1 = (groupBaseIndex * 2) + 1; const groupNum2 = (groupBaseIndex * 2) + 2; generatedRules.push({ name: `Play-Out Grup ${isTopLeague ? 'A' : groupNum1} ${groupNameSuffix}`, type: 'playout', teams: [ { league: currentLeagueName, rank: 8 }, { league: childLeague1, rank: 2 }, { league: childLeague2, rank: 3 }, { league: childLeague3, rank: 3 } ] }); generatedRules.push({ name: `Play-Out Grup ${isTopLeague ? 'B' : groupNum2} ${groupNameSuffix}`, type: 'playout', teams: [ { league: currentLeagueName, rank: 9 }, { league: childLeague1, rank: 3 }, { league: childLeague2, rank: 2 }, { league: childLeague3, rank: 2 } ] }); } const uniqueRules = []; const seenNames = new Set(); for (const rule of generatedRules) { if (!seenNames.has(rule.name)) { uniqueRules.push(rule); seenNames.add(rule.name); } } if (isTopLeague) { return uniqueRules.filter(rule => rule.type === 'playout'); } return uniqueRules; } function findRelevantRules(currentLeagueName) { const leagueInfo = parseLeagueName(currentLeagueName); if (!leagueInfo) { console.warn("Mevcut lig ayrıştırılamadı:", currentLeagueName); return []; } if (leagueInfo.level >= 7) { return []; } return generateRules(leagueInfo.level, leagueInfo.index, currentLeagueName, leagueInfo.isTopLeague); } async function startJob() { if (isJobRunning) { alert("Zaten devam eden bir işlem var. Lütfen tamamlanmasını bekleyin."); return; } isJobRunning = true; showProgressIndicator("İşlem Başlatılıyor..."); const leagueSelect = getLeagueSelect(); if (!leagueSelect) { alert("Lig seçme menüsü bulunamadı!"); isJobRunning = false; removeProgressIndicator(); return; } const currentLeagueName = leagueSelect.options[leagueSelect.selectedIndex].text; const relevantRules = findRelevantRules(currentLeagueName); if (!relevantRules || relevantRules.length === 0) { let predictorContainer = document.getElementById('playoff-predictor-container'); if (!predictorContainer) { const mainContentArea = document.querySelector('#league_navigation > div.ui-tabs-panel'); predictorContainer = setupContainer(mainContentArea); } predictorContainer.innerHTML = `<div style="padding: 15px; background-color: #f8f9fa; border: 1px solid #dee2e6; text-align: center; margin-top: 20px;">Bu lig (${currentLeagueName}) için gösterilecek Play-Off/Out kuralı bulunmuyor.</div>`; isJobRunning = false; removeProgressIndicator(); return; } const allTeamDefs = getUniqueTeamDefs(relevantRules); const leagueIdMap = createLeagueIdMap(leagueSelect); const collectedData = []; let hiddenIframe = null; try { hiddenIframe = document.createElement('iframe'); hiddenIframe.style.cssText = 'display: none !important;'; document.body.appendChild(hiddenIframe); const teamDefsToFetchInIframe = allTeamDefs.filter(def => def.league.toLowerCase() !== currentLeagueName.toLowerCase()); const teamDefsOnCurrentPage = allTeamDefs.filter(def => def.league.toLowerCase() === currentLeagueName.toLowerCase()); teamDefsOnCurrentPage.forEach(def => { const teamData = parseTeamFromDocument(document, def.rank, window.location.href); if (teamData) collectedData.push({ ...teamData, league: def.league }); }); let leaguesProcessed = teamDefsOnCurrentPage.length; for (const teamDef of teamDefsToFetchInIframe) { if (!isJobRunning) { throw new Error("İşlem kullanıcı tarafından iptal edildi."); } updateProgressIndicator(`Lig Verileri Toplanıyor... (${leaguesProcessed}/${allTeamDefs.length}) - ${teamDef.league}`); const teamData = await fetchLeagueDataViaIframe(teamDef, leagueIdMap, hiddenIframe); collectedData.push(teamData); leaguesProcessed++; } updateProgressIndicator("Lig verileri toplandı. Takım değerleri hesaplanıyor..."); let teamsWithValueProcessed = 0; const validTeams = collectedData.filter(d => d && d.url && d.url !== '#'); const totalTeamsWithValue = validTeams.length; const valuePromises = validTeams.map(team => { if (!isJobRunning) return Promise.reject(new Error("İşlem iptal edildi.")); return fetchTop11Value(team).then(result => { teamsWithValueProcessed++; updateProgressIndicator(`En İyi 11 Değerleri Hesaplanıyor... (${teamsWithValueProcessed}/${totalTeamsWithValue})`); return result; }); }); const finalDataWithValues = await Promise.all(valuePromises); const finalData = collectedData.map(team => { const teamWithValue = finalDataWithValues.find(t => t && team && t.url === team.url); return teamWithValue || team; }); if (!isJobRunning) return; updateProgressIndicator("Tablo Oluşturuluyor..."); const mainContentArea = document.querySelector('#league_navigation > div.ui-tabs-panel'); let predictorContainer = setupContainer(mainContentArea); const finalHtml = createTablesHTML(relevantRules, finalData); predictorContainer.innerHTML = finalHtml; } catch (error) { if (error.message.includes("iptal edildi")) { console.log("İşlem durduruldu."); } else { console.error("Betiğin çalışması sırasında bir hata oluştu:", error); alert("Betiğin çalışması sırasında bir hata oluştu. Lütfen konsolu kontrol edin."); } } finally { if (hiddenIframe) hiddenIframe.remove(); removeProgressIndicator(); isJobRunning = false; } } function fetchLeagueDataViaIframe(teamDef, leagueIdMap, iframe) { return new Promise((resolve) => { const leagueNameLower = teamDef.league.toLowerCase(); const sid = leagueIdMap[leagueNameLower]; if (!sid) { console.warn(`Lig ID bulunamadı: ${teamDef.league}`); resolve({ rank: teamDef.rank, league: teamDef.league, name: `(Lig bulunamadı)`, url: '#' }); return; } const url = `https://www.managerzone.com/?p=league&type=senior&sid=${sid}`; let poller = null; let timeout = null; const cleanup = () => { if (poller) clearInterval(poller); if (timeout) clearTimeout(timeout); iframe.onload = null; iframe.onerror = null; }; const fail = (errorType) => { console.warn(`${errorType} for league: ${teamDef.league}`); cleanup(); resolve({ rank: teamDef.rank, league: teamDef.league, name: `(${errorType})`, url: '#' }); }; timeout = setTimeout(() => fail('Timeout Hatası'), 15000); iframe.onload = () => { poller = setInterval(() => { try { const doc = iframe.contentDocument; if (doc && doc.querySelector('.nice_table tbody tr')) { cleanup(); const teamData = parseTeamFromDocument(doc, teamDef.rank, url); resolve({ ...teamData, league: teamDef.league }); } } catch (e) { // ignore error } }, 300); }; iframe.onerror = () => fail('Yükleme Hatası'); iframe.src = url; }); } function fetchTop11Value(team) { if (!team || !team.url) return Promise.resolve({ ...team, top11Value: 'URL Yok' }); const teamId = new URL(team.url).searchParams.get('tid'); if (!teamId) return Promise.resolve({ ...team, top11Value: 'ID Yok' }); const playersUrl = `https://www.managerzone.com/?p=players&sub=alt&tid=${teamId}`; return fetch(playersUrl) .then(response => { if (!response.ok) throw new Error(`HTTP hatası! Durum: ${response.status}`); return response.text(); }) .then(html => ({ ...team, top11Value: calculateTop11ValueFromHTML(html) })) .catch(error => { console.error(`Takım ID ${teamId} için "En İyi 11" değeri alınamadı:`, error); return { ...team, top11Value: 'Hata' }; }); } function waitForElement(selector, callback) { const i = setInterval(() => { const e = document.querySelector(selector); if (e) { clearInterval(i); callback(); } }, 100); } function createLeagueIdMap(leagueSelect) { const map = {}; for (const o of leagueSelect.options) { map[o.text.toLowerCase().trim()] = o.value; } return map; } function setupContainer(parent) { let c = document.getElementById('playoff-predictor-container'); if (c) { c.innerHTML = ''; } else { c = document.createElement('div'); c.id = 'playoff-predictor-container'; const t = parent.querySelector('.nice_table'); if (t) t.parentNode.insertAdjacentElement('afterend', c); else parent.appendChild(c); } return c; } function getLeagueSelect() { const s = document.querySelectorAll('select[onchange*="p=league&type=senior&sid="]'); return s.length > 1 ? s[1] : s[0]; } function getUniqueTeamDefs(rules) { const u = []; const seen = new Set(); rules.forEach(g => { g.teams.forEach(t => { const key = `${t.league}-${t.rank}`; if (!seen.has(key)) { u.push(t); seen.add(key); } }); }); return u; } function parseTeamFromDocument(doc, rank, sourceUrl = 'Bilinmiyor') { if (!doc) { console.warn(`Belge (document) yok. Sıra: ${rank}, Kaynak: ${sourceUrl}`); return { rank, name: `(Belge Okunamadı)`, url: '#' }; } const tr = [...doc.querySelectorAll('.nice_table tbody tr')].find(r => r.cells[0]?.textContent.trim() == rank); if (tr && tr.cells[1]) { const a = tr.cells[1].querySelector('a[href*="p=team&tid="]'); if (a) { const stats = { g: tr.cells[3]?.textContent.trim(), b: tr.cells[4]?.textContent.trim(), m: tr.cells[5]?.textContent.trim(), a: tr.cells[6]?.textContent.trim(), y: tr.cells[7]?.textContent.trim(), av: tr.cells[8]?.textContent.trim(), p: tr.cells[9]?.textContent.trim() }; return { rank, name: a.textContent.trim(), url: a.href, ...stats }; } } console.warn(`Takım bulunamadı. Sıra: ${rank}, Kaynak: ${sourceUrl}`); return { rank, name: `(Sıra ${rank} bulunamadı)`, url: '#' }; } // DEĞİŞİKLİK BAŞLANGICI: Evrensel Değer Sütunu Tespiti function calculateTop11ValueFromHTML(htmlText) { try { const doc = new DOMParser().parseFromString(htmlText, 'text/html'); const playerTable = doc.getElementById('playerAltViewTable'); if (!playerTable) return 'Tablo Yok'; const firstDataRow = playerTable.querySelector('tbody tr'); if (!firstDataRow) return 'Oyuncu Yok'; let valueColumnIndex = -1; const headers = playerTable.querySelectorAll('thead th'); const firstDataRowCells = firstDataRow.cells; // 1. Strateji: Başlıklarda "Değer" veya benzeri bir kelime ara (Regex ile evrensel) if (headers.length > 0) { for (let i = 0; i < headers.length; i++) { if (/value|değer|wert|valore|valor/i.test(headers[i].textContent)) { valueColumnIndex = i; break; } } } // 2. Strateji: Başlık bulunamazsa, ilk veri satırında para birimi ara if (valueColumnIndex === -1 && firstDataRowCells) { for (let i = 0; i < firstDataRowCells.length; i++) { if (/\s(EUR|USD|TRY)/.test(firstDataRowCells[i].textContent)) { valueColumnIndex = i; break; } } } if (valueColumnIndex === -1) { console.warn("Değer sütunu bulunamadı."); return 'Sütun Yok'; } const values = []; playerTable.querySelectorAll('tbody tr').forEach(row => { const cell = row.cells[valueColumnIndex]; if (cell) { const numericValue = parseInt(cell.textContent.trim().replace(/[^0-9]/g, ''), 10); if (!isNaN(numericValue)) { values.push(numericValue); } } }); if (values.length === 0) return 'Değer Yok'; values.sort((a, b) => b - a); const sum = values.slice(0, 11).reduce((acc, val) => acc + val, 0); return `${sum.toLocaleString('de-DE')} EUR`; } catch (e) { console.error("HTML parse/hesaplama hatası:", e); return "Hesaplama Hatası"; } } // DEĞİŞİKLİK SONU function createTablesHTML(rules, data) { const playoffRules = rules.filter(r => r.type === 'playoff'); const playoutRules = rules.filter(r => r.type === 'playout'); let h = `<h2 class="subheader clearfix" style="margin-top: 20px;">Potansiyel Play-Off / Play-Out Grupları</h2><div style="font-size: 0.9em; padding: 5px; background-color: #fff3cd; border: 1px solid #ffeeba; border-radius: 4px; margin-bottom: 10px; color: #856404;"><strong>Not:</strong> Bu tablolar, anlık puan durumlarına göre oluşturulmuş bir <strong>tahmindir</strong> ve resmi değildir.</div>`; const generateHtmlForRules = (ruleSet) => { let partialHtml = ''; ruleSet.forEach(g => { partialHtml += `<h3 class="subheader clearfix">${g.name}</h3><div class="mainContent" style="padding: 5px; margin-bottom: 20px;"><table class="nice_table" style="width:100%;"> <thead><tr><th style="width:13%;">Lig</th><th style="width:5%; text-align: center;">Sıra</th><th style="width:18%;">Potansiyel Takım</th><th title="Oynanan Maç" style="width:4%; text-align: center;">O</th><th title="Galibiyet" style="width:4%; text-align: center;">G</th><th title="Beraberlik" style="width:4%; text-align: center;">B</th><th title="Mağlubiyet" style="width:4%; text-align: center;">M</th><th title="Attığı Gol" style="width:4%; text-align: center;">A</th><th title="Yediği Gol" style="width:4%; text-align: center;">Y</th><th title="Averaj" style="width:4%; text-align: center;">Av</th><th title="Puan" style="width:5%; text-align: center;">P</th><th style="width:15%; text-align: center;">En Değerli 11</th></tr></thead><tbody>`; g.teams.forEach(t => { const d = data.find(ad => ad && ad.league && t.league && ad.league.toLowerCase() === t.league.toLowerCase() && ad.rank === t.rank); let teamCell = `...`; let playedMatchesCell = '<td style="text-align:center;">-</td>'; let statsCells = '<td colspan="7" style="text-align:center;">- Veri Yok -</td>'; // Colspan düzeltildi let top11Cell = '<td>...</td>'; let rankCell = `<td style="text-align: center;">${t.rank}</td>`; if (d) { teamCell = d.url && d.url !== '#' ? `<a href="${d.url}" target="_blank" title="${d.name}">${d.name}</a>` : d.name; if (d.p !== undefined) { const g = parseInt(d.g, 10) || 0; const b = parseInt(d.b, 10) || 0; const m = parseInt(d.m, 10) || 0; const o = g + b + m; // DEĞİŞİKLİK: 'O' sütunundaki <b> etiketi kaldırıldı. playedMatchesCell = `<td style="text-align: center;">${o}</td>`; // DEĞİŞİKLİK: 'P' sütunundaki <b> etiketi kaldırıldı. statsCells = `<td style="text-align: center;">${d.g || '-'}</td><td style="text-align: center;">${d.b || '-'}</td><td style="text-align: center;">${d.m || '-'}</td><td style="text-align: center;">${d.a || '-'}</td><td style="text-align: center;">${d.y || '-'}</td><td style="text-align: center;">${d.av || '-'}</td><td style="text-align: center;">${d.p}</td>`; } top11Cell = `<td style="text-align: center;">${d.top11Value || 'Hesaplanıyor...'}</td>`; } else { teamCell = `<span style="color:red;">(Veri alınamadı)</span>`; } partialHtml += `<tr><td>${t.league}</td>${rankCell}<td>${teamCell}</td>${playedMatchesCell}${statsCells}${top11Cell}</tr>`; }); partialHtml += `</tbody></table></div>`; }); return partialHtml; }; if (playoffRules.length > 0) { h += generateHtmlForRules(playoffRules); } if (playoutRules.length > 0) { h += generateHtmlForRules(playoutRules); } h += `<hr><div style="font-size:0.9em; padding:10px; background-color: #f0f0f0; border: 1px solid #ddd; border-radius: 4px;"><h4>Bilgi</h4><p>Yukarıdaki puan tablosunun kesin olmadığını, takım pozisyonlarının çekişmeli olması durumunda play-off/out gruplarının değişebileceğini unutmayın. Olası eşitlik durumlarında sıralamaların hesaplanması için aşağıdaki öncelik sırasına göre bir dizi kural uygulanır:</p><ul><li><strong>Puanı en yüksek olan takım:</strong> En temel sıralama ölçütüdür.</li><li><strong>Averaj:</strong> Puan eşitliğinde averajı daha iyi olan takım üstte yer alır.</li><li><strong>Sezon içi galibiyet sayısı:</strong> Puan ve averaj eşitliğinde daha fazla galibiyeti olan takım önceliklidir.</li><li><strong>Gol sayısı:</strong> Yukarıdaki tüm kriterler eşitse, daha fazla gol atan takım üstte yer alır.</li><li><strong>Kura çekimi:</strong> Tüm istatistiklerin tamamen aynı olması gibi çok nadir bir durumda sıralama kura ile belirlenir.</li></ul></div>`; return h; } function showProgressIndicator(message) { removeProgressIndicator(); const i = document.createElement('div'); i.id = 'mz-predictor-progress'; i.style = "position: fixed; top: 0; left: 0; width: 100%; background: #00529B; color: white; padding: 10px; z-index: 9999; text-align: center; font-size: 1.1em; border-bottom: 2px solid #003666; box-shadow: 0 2px 5px rgba(0,0,0,0.5);"; i.innerHTML = `<span><i class="fa fa-spinner fa-pulse"></i> ${message}</span> <button id="cancelJobBtn" style="margin-left: 20px; background: #c00; color: white; border: 1px solid white; border-radius: 4px; cursor: pointer; padding: 2px 8px;">Durdur</button>`; document.body.appendChild(i); document.getElementById('cancelJobBtn').onclick = () => { isJobRunning = false; }; } function updateProgressIndicator(message) { const i = document.getElementById('mz-predictor-progress'); if (i) i.querySelector('span').innerHTML = `<i class="fa fa-spinner fa-pulse"></i> ${message}`; } function removeProgressIndicator() { const i = document.getElementById('mz-predictor-progress'); if (i) i.remove(); } function initButton() { const container = document.querySelector('div[style*="float: right; line-height: 36px;"]'); if (container && !document.getElementById('runPredictorBtn')) { const btn = document.createElement('a'); btn.href = '#'; btn.className = 'mzbtn buttondiv button_account'; btn.id = 'runPredictorBtn'; btn.style.marginLeft = '10px'; btn.innerHTML = `<span class="buttonClassMiddle"><span style="white-space: nowrap;"><i class="fa fa-sitemap" aria-hidden="true"></i> Play-Off/Out Tahmin Et</span></span><span class="buttonClassRight"> </span>`; btn.addEventListener('click', (e) => { e.preventDefault(); startJob(); }); container.appendChild(btn); } } waitForElement('div[style*="float: right; line-height: 36px;"]', initButton); } /**************************************************************************************** * * * BÖLÜM 5: TAKİP LİSTESİ FİLTRELEME (Shortlist Filter) - DÜZELTİLDİ v4 (SON VERSİYON) * * * ****************************************************************************************/ function initializeShortlistFilterScript() { let userCountryCode = null; const shortlistLocale = document.querySelector('meta[name="language"]')?.getAttribute('content')?.toLowerCase() || 'en'; const shortlistText = shortlistLocale.startsWith('es') ? { title: 'Filtrar jugadores', ageRange: 'Rango de edad', minAge: 'Mín.', maxAge: 'Máx.', other: 'Otros', retiringOnly: 'Próximos a retirarse', onTransferOnly: 'En transferencia (activa)', expiredTransferOnly: 'Transferencias vencidas', nationality: 'Nacionalidad', all: 'Todos', national: 'Nacionales', foreign: 'Extranjeros', apply: 'Filtrar', reset: 'Restablecer', playersFiltered: (count) => count + ' jugadores filtrados.', transferClockError: 'Error: no se pudo leer la hora del juego. No es posible aplicar el filtro de transferencias.' } : { title: 'Filter players', ageRange: 'Age range', minAge: 'Min.', maxAge: 'Max.', other: 'Other', retiringOnly: 'Retiring', onTransferOnly: 'On transfer (active)', expiredTransferOnly: 'Expired transfers', nationality: 'Nationality', all: 'All', national: 'National', foreign: 'Foreign', apply: 'Filter', reset: 'Reset', playersFiltered: (count) => count + ' players filtered.', transferClockError: 'Error: the game clock could not be read. The transfer filter cannot be applied.' }; const i18n = { tr: { downloadImageTitle: "Oyuncu profilini resim olarak indir", downloading: "İndiriliyor...", downloadError: "Oyuncu resmi indirilirken bir hata oluştu.", playerDataNotFound: "Resme dönüştürülecek oyuncu verisi bulunamadı." }, en: { downloadImageTitle: "Download player profile as image", downloading: "Downloading...", downloadError: "An error occurred while downloading the player image.", playerDataNotFound: "Could not find player data to capture." } }; const lang = Core.i18n.lang; const getText = (key) => i18n[lang][key]; function gmRequest(options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...options, onload: (response) => resolve(response), onerror: (error) => reject(error), ontimeout: (error) => reject(error) }); }); } async function fetchAndSetUserCountryCode() { if (userCountryCode !== null) return userCountryCode; try { const cachedCode = await GM_getValue('user_country_code'); if (cachedCode) { userCountryCode = cachedCode; return userCountryCode; } const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "https://www.managerzone.com/?p=team", onload: resolve, onerror: reject }); }); const html = response.responseText; const doc = new DOMParser().parseFromString(html, 'text/html'); const countryImg = doc.querySelector('#infoAboutTeam img[src*="img/flags/s_"]'); if (countryImg) { const src = countryImg.getAttribute('src'); const match = src.match(/s_([a-z]{2})\.gif/); if (match && match[1]) { userCountryCode = match[1]; await GM_setValue('user_country_code', userCountryCode); } else { userCountryCode = ''; } } else { userCountryCode = ''; } } catch (error) { console.error("Kullanıcı ülke kodu alınırken hata oluştu:", error); userCountryCode = ''; } return userCountryCode; } // Bu fonksiyon, HTML'den gelen metinleri tarih nesnesine çevirir. function parseMzDateTime(dateTimeString) { if (!dateTimeString) return null; // Hem "DD-MM-YYYY HH:MM" hem de "DD-MM-YYYY HH:MM:SS" formatlarını yakalar const parts = dateTimeString.match(/(\d{2})-(\d{2})-(\d{4})\s(\d{2}):(\d{2}):?(\d{2})?/); if (!parts) return null; const day = parseInt(parts[1], 10); const month = parseInt(parts[2], 10) - 1; // JS'de aylar 0'dan başlar const year = parseInt(parts[3], 10); const hour = parseInt(parts[4], 10); const minute = parseInt(parts[5], 10); const second = parts[6] ? parseInt(parts[6], 10) : 0; return new Date(year, month, day, hour, minute, second); } // En güvenilir saat alma yöntemi: Sayfanın kendi JS değişkeninden okur. function getGameTime() { // `unsafeWindow` kullanarak sayfanın global scope'undaki değişkenlere erişiyoruz. if (typeof unsafeWindow.server_time === 'object' && unsafeWindow.server_time instanceof Date) { return unsafeWindow.server_time; } // Yedek olarak HTML'den okumayı dener. const timeElement = document.querySelector('#header-stats-wrapper h5:first-child'); if (timeElement) { const match = timeElement.textContent.match(/(\d{2}-\d{2}-\d{4}\s\d{2}:\d{2})/); if (match && match[1]) { return parseMzDateTime(match[1]); } } console.error("KRİTİK HATA: Oyun saati alınamadı."); return null; } async function handlePlayerImageDownload(event) { const button = $(event.currentTarget); const pid = button.data('pid'); const playerName = button.data('player-name'); const originalIcon = button.html(); button.html('⏳').prop('disabled', true).attr('title', getText('downloading')); const iframe = document.createElement('iframe'); iframe.style.position = 'absolute'; iframe.style.top = '-9999px'; iframe.style.left = '-9999px'; iframe.style.border = 'none'; iframe.style.width = '1200px'; try { const sourceElement = button.closest('.playerContainer')[0]; if (!sourceElement) { throw new Error("Kaynak oyuncu konteyneri bulunamadı."); } const sourceRect = sourceElement.getBoundingClientRect(); const sourceWidth = sourceRect.width; document.body.appendChild(iframe); await new Promise(resolve => { iframe.onload = resolve; iframe.src = 'about:blank'; }); const iframeDoc = iframe.contentDocument; if (!iframeDoc) { throw new Error("Iframe içeriğine erişilemedi."); } let allStyles = ''; document.querySelectorAll('link[rel="stylesheet"], style').forEach(styleEl => { allStyles += styleEl.outerHTML; }); const iframeHtml = ` <!DOCTYPE html> <html> <head> <base href="https://www.managerzone.com/"> ${allStyles} <style> body { /* Arka plan rengini burada da ayarlamak, render tutarlılığına yardımcı olabilir */ background-color: #e6e6e6 !important; margin: 20px; } .capture-wrapper { width: ${sourceWidth}px !important; } </style> </head> <body> <div class="capture-wrapper"> ${sourceElement.outerHTML} </div> </body> </html> `; iframeDoc.open(); iframeDoc.write(iframeHtml); iframeDoc.close(); await new Promise(r => setTimeout(r, 1500)); const targetElementInIframe = iframeDoc.querySelector('.playerContainer'); if (!targetElementInIframe) { throw new Error("Oyuncu konteyneri iframe içine kopyalanamadı."); } const transferDisclaimer = targetElementInIframe.querySelector('.bid-history-lite-wrapper p'); if (transferDisclaimer) { transferDisclaimer.remove(); } // ▼▼▼ EN ÖNEMLİ DEĞİŞİKLİK BURADA ▼▼▼ // Arka plan rengini, istediğimiz açık gri renk (#e6e6e6) olarak açıkça belirtiyoruz. const canvas = await html2canvas(targetElementInIframe, { useCORS: true, allowTaint: true, scale: 1.5, backgroundColor: '#e6e6e6' // <-- DÜZELTME BURADA }); // ▲▲▲ DEĞİŞİKLİK SONA ERDİ ▲▲▲ const link = document.createElement('a'); link.download = `MZ_Player_${playerName.replace(/\s/g, '_')}_${pid}.png`; link.href = canvas.toDataURL('image/png'); link.click(); } catch (error) { console.error(getText('downloadError'), error); alert(getText('downloadError')); } finally { if (iframe) { iframe.remove(); } button.html(originalIcon).prop('disabled', false).attr('title', getText('downloadImageTitle')); } } function addFilterUI() { GM_addStyle(` /* ... stiller aynı kalabilir ... */ .sl-action-btn { padding: 5px 12px !important; cursor: pointer; color: white !important; border: 1px solid rgba(0,0,0,0.2) !important; border-radius: 4px !important; font-weight: bold !important; font-size: 13px !important; text-decoration: none !important; display: inline-block !important; line-height: normal !important; height: auto !important; } #shortlist-filter-btn { background-color: #4CAF50; } #shortlist-filter-btn:hover { background-color: #45a049; } #script-controls-wrapper { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; padding: 10px; background: #f1f1f1; border: 1px solid #ddd; border-radius: 5px; } #shortlist-filter-controls { display: flex; align-items: center; gap: 5px; } #shortlist-filter-count { font-weight: bold; color: #00529B; } .sl-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); z-index: 10010; display: none; justify-content: center; align-items: center; } .sl-modal-content { background: #fefefe; padding: 20px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); min-width: 300px; display: flex; flex-direction: column; max-width: 450px; } .sl-modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #ddd; padding-bottom: 10px; margin-bottom: 15px; } .sl-modal-header h3 { margin: 0; font-size: 18px; } .sl-modal-close { font-size: 24px; font-weight: bold; cursor: pointer; color: #888; } .sl-modal-body { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; } .sl-filter-group { border: 1px solid #ddd; padding: 10px; border-radius: 5px; background: #fafafa; } .sl-filter-group h4 { margin-top: 0; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 5px; } .sl-filter-group label { display: block; margin-bottom: 5px; } .sl-age-inputs { display: flex; align-items: center; gap: 5px; } .sl-modal-content input[type="number"] { width: 60px; padding: 5px; border: 1px solid #ccc; border-radius: 3px; } .sl-modal-footer { border-top: 1px solid #ddd; padding-top: 15px; margin-top: 20px; text-align: center; } .sl-modal-footer button { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; color: white; margin: 0 5px; } #shortlist-filter-apply { background-color: #007bff; } #shortlist-filter-reset { background-color: #6c757d; } .player-download-btn { padding: 0px 7px; margin-left: 8px; font-weight: bold; cursor: pointer; background-color: #28a745; color: white; border: 1px solid #1e7e34; border-radius: 4px; font-size: 12px; vertical-align: middle; } `); const modalHTML = ` <div id="shortlist-filter-modal" class="sl-modal-overlay"> <div class="sl-modal-content"> <div class="sl-modal-header"> <h3>${shortlistText.title}</h3> <span class="sl-modal-close">×</span> </div> <div class="sl-modal-body"> <div class="sl-filter-group"> <h4>${shortlistText.ageRange}</h4> <div class="sl-age-inputs"> <input type="number" id="min-age" placeholder="${shortlistText.minAge}" min="15" max="40"> <span>-</span> <input type="number" id="max-age" placeholder="${shortlistText.maxAge}" min="15" max="40"> </div> </div> <div class="sl-filter-group"> <h4>${shortlistText.other}</h4> <label> <input type="checkbox" id="retiring-only"> ${shortlistText.retiringOnly} </label> <label> <input type="checkbox" id="on-transfer-only"> ${shortlistText.onTransferOnly} </label> <label> <input type="checkbox" id="expired-transfer-only"> ${shortlistText.expiredTransferOnly} </label> </div> <div class="sl-filter-group" style="grid-column: 1 / -1;"> <h4>${shortlistText.nationality}</h4> <label><input type="radio" name="nationality" value="all" checked> ${shortlistText.all}</label> <label><input type="radio" name="nationality" value="national"> ${shortlistText.national}</label> <label><input type="radio" name="nationality" value="foreign"> ${shortlistText.foreign}</label> </div> </div> <div class="sl-modal-footer"> <button id="shortlist-filter-apply">${shortlistText.apply}</button> <button id="shortlist-filter-reset">${shortlistText.reset}</button> </div> </div> </div> `; $('body').append(modalHTML); const playersListContainer = $('#players_container'); if (playersListContainer.length > 0 && $('#script-controls-wrapper').length === 0) { const scriptWrapper = $(`<div id="script-controls-wrapper"><div id="shortlist-filter-controls"><button id="shortlist-filter-btn" class="sl-action-btn">${shortlistText.apply}</button><span id="shortlist-filter-count"></span></div></div>`); playersListContainer.before(scriptWrapper); } $('#shortlist-filter-btn').on('click', () => $('#shortlist-filter-modal').css('display', 'flex')); $('#shortlist-filter-modal .sl-modal-close').on('click', () => $('#shortlist-filter-modal').hide()); $('#shortlist-filter-apply').on('click', applyFilters); $('#shortlist-filter-reset').on('click', resetFilters); $('#shortlist-filter-modal input[type="number"]').on('keydown keyup keypress', event => event.stopPropagation()); } function applyFilters() { const minAge = parseInt($('#min-age').val(), 10) || 0; const maxAge = parseInt($('#max-age').val(), 10) || 100; const retiringOnly = $('#retiring-only').is(':checked'); const onTransferOnly = $('#on-transfer-only').is(':checked'); const expiredTransferOnly = $('#expired-transfer-only').is(':checked'); const nationalityFilter = $('input[name="nationality"]:checked').val(); const currentGameTime = getGameTime(); if (!currentGameTime && (onTransferOnly || expiredTransferOnly)) { alert(shortlistText.transferClockError); return; } let visibleCount = 0; $('.playerContainer').each(function () { const player = $(this); let show = true; // Önceki filtreler (yaş, emeklilik vb.) if (show) { let age = -1; player.find('.dg_playerview_info table td').each(function () { const text = $(this).text().toLowerCase(); if (/yaş:|age:|edad:|âge:|ålder:|idade:|età:|alter:|wiek:/i.test(text)) { const ageMatch = text.match(/\d+/); if (ageMatch) { age = parseInt(ageMatch[0], 10); return false; } } }); if (age !== -1 && (age < minAge || age > maxAge)) { show = false; } } if (show && retiringOnly) { if (player.find('.dg_playerview_retire').length === 0) { show = false; } } // *** YENİ ve DÜZELTİLMİŞ TRANSFER FİLTRELEME MANTIĞI (v2 - VEYA Mantığı) *** if (show && (onTransferOnly || expiredTransferOnly)) { let isPlayerOnTransfer = false; let isTransferActive = false; // "Bitiş tarihi:" (veya diğer dillerdeki karşılıkları) etiketini içeren <span> elementini bul const deadlineLabel = player.find('span').filter(function () { const text = $(this).text().trim().toLowerCase(); return text.includes('tarihi:') || text.includes('deadline:') || text.includes('plazo:') || text.includes('limite:') || text.includes('fecha:') || text.includes('gräns:'); }); if (deadlineLabel.length > 0) { // Etiketin bir sonraki kardeşi olan <span> içindeki metni al. const rawDeadlineText = deadlineLabel.next('span').text(); // Regex ile sadece tarih ve saat kısmını güvenli bir şekilde al. const match = rawDeadlineText.match(/(\d{2}-\d{2}-\d{4}\s\d{2}:\d{2}(?::\d{2})?)/); if (match && match[1]) { const deadlineTime = parseMzDateTime(match[1]); if (deadlineTime) { isPlayerOnTransfer = true; // Oyun saati ile karşılaştır. isTransferActive = deadlineTime > currentGameTime; } } } // --- HATA DÜZELTMESİ BAŞLANGICI --- // Oyuncunun seçili kriterlerden herhangi birine uyup uymadığını kontrol etmek için bir değişken. let matchesTransferCriteria = false; // 1. "Aktif" filtresi seçiliyse ve oyuncu aktif transferdeyse, kriteri karşılar. if (onTransferOnly && isPlayerOnTransfer && isTransferActive) { matchesTransferCriteria = true; } // 2. "Bitmiş" filtresi seçiliyse ve oyuncunun transferi bitmişse, kriteri karşılar. // Bu bir VEYA (OR) durumu olduğu için, önceki koşul doğru olsa bile bunu kontrol ederiz. if (expiredTransferOnly && isPlayerOnTransfer && !isTransferActive) { matchesTransferCriteria = true; } // Eğer her iki filtre de seçiliyse, bu blok zaten yukarıdaki iki if bloğu sayesinde // doğru çalışacaktır. Oyuncu ya aktif ya da bitmiş transferdeyse matchesTransferCriteria = true olur. // Eğer sadece bir filtre seçiliyse ve oyuncu o kritere uymuyorsa matchesTransferCriteria = false kalır. // Eğer oyuncu, seçilen transfer kriterlerinden HİÇBİRİNİ karşılamıyorsa gizle. if (!matchesTransferCriteria) { show = false; } // --- HATA DÜZELTMESİ SONU --- } // Milliyet filtresi if (show && userCountryCode && nationalityFilter !== 'all') { const playerFlagImg = player.find('img[src*="img/flags/s_"]'); let playerCountryCode = null; if (playerFlagImg.length > 0) { const src = playerFlagImg.attr('src'); const match = src.match(/s_([a-z]{2})\.gif/); if (match && match[1]) { playerCountryCode = match[1]; } } if (playerCountryCode) { if (nationalityFilter === 'national' && playerCountryCode !== userCountryCode) { show = false; } if (nationalityFilter === 'foreign' && playerCountryCode === userCountryCode) { show = false; } } } player.toggle(show); if (show) { visibleCount++; } }); const isFiltered = minAge > 0 || maxAge < 100 || retiringOnly || onTransferOnly || expiredTransferOnly || nationalityFilter !== 'all'; $('#shortlist-filter-count').text(isFiltered ? shortlistText.playersFiltered(visibleCount) : ''); $('#shortlist-filter-modal').hide(); } function resetFilters() { $('#min-age').val(''); $('#max-age').val(''); $('#retiring-only').prop('checked', false); $('#on-transfer-only').prop('checked', false); $('#expired-transfer-only').prop('checked', false); $('input[name="nationality"][value="all"]').prop('checked', true); $('.playerContainer').show(); $('#shortlist-filter-count').text(''); $('#shortlist-filter-modal').hide(); } function init() { if ($('#players_container').length) { fetchAndSetUserCountryCode(); addFilterUI(); // GÜNCELLENDİ: İndirme butonunu her oyuncu için ekliyoruz $('.playerContainer').each(function () { const pid = $(this).find('span.player_id_span').text().trim(); const playerName = $(this).find('span.player_name').text().trim(); // Eğer buton zaten varsa tekrar ekleme if (pid && $(this).find('.player-download-btn').length === 0) { // ▼▼▼ DEĞİŞTİRİLECEK SATIR BURASI ▼▼▼ const downloadBtn = $(`<button class="player-download-btn" title="${getText('downloadImageTitle')}"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; pointer-events: none;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg></button>`); // ▲▲▲ DEĞİŞTİRİLECEK SATIR BURASI ▲▲▲ downloadBtn.data('pid', pid).data('player-name', playerName); // Butonu mevcut Transfer Takipçisi butonunun yanına ekle const actionContainer = $(this).find('h2.subheader .floatRight'); if (actionContainer.length > 0) { actionContainer.append(downloadBtn); } } }); // YENİ EKLENDİ: Butonlara tıklama olayını atıyoruz. // Sayfa dinamik olarak değişebileceği için 'document' üzerinden olay atamak daha güvenlidir. $(document).off('click', '.player-download-btn').on('click', '.player-download-btn', handlePlayerImageDownload); } } init(); } /**************************************************************************************** * * * BÖLÜM 6: TRANSFER TAKİPÇİSİ (Transfer Tracker) - YENİ MODÜL * * * ****************************************************************************************/ function initializeTransferTrackerScript() { const i18nData = { tr: { trackPlayer: "Oyuncunun Transfer Geçmişini Takip Et", historyFor: "için Transfer Geçmişi", checking: "Kontrol ediliyor...", noHistory: "Bu oyuncu için kayıtlı transfer geçmişi yok.", clearHistory: "Geçmişi Temizle", confirmClear: "Bu oyuncunun tüm transfer geçmişi silinecek. Emin misiniz?", date: "Tarih", status: "Durum", price: "Fiyat", bid: "Teklif", deadline: "Bitiş", buyer: "Alıcı", statusListed: "Listelendi", statusSold: "Satıldı", statusExpired: "Satılmadı", statusEnded: "Bitti (Kontrol bekleniyor)", statusBid: "Teklif Var", checkingShortlist: "Takip listesi transferleri kontrol ediliyor...", checkComplete: "Transfer kontrolü tamamlandı.", scanShortlistBtn: "Takip Listesini Tara", }, en: { trackPlayer: "Track Player's Transfer History", historyFor: "Transfer History for", checking: "Checking...", noHistory: "No transfer history recorded for this player.", clearHistory: "Clear History", confirmClear: "Are you sure you want to delete all transfer history for this player?", date: "Date", status: "Status", price: "Price", bid: "Bid", deadline: "Deadline", buyer: "Buyer", statusListed: "Listed", statusSold: "Sold", statusExpired: "Expired", statusEnded: "Ended (Awaiting check)", statusBid: "Bid Placed", checkingShortlist: "Checking shortlist transfers...", checkComplete: "Transfer check complete.", scanShortlistBtn: "Scan Transfers", } }; const lang = Core.i18n.lang; const getText = (key) => i18nData[lang][key]; const TRANSFER_HISTORY_KEY = 'mz_transfer_history_v2'; GM_addStyle(` .transfer-track-btn { padding: 0px 7px; margin-left: 8px; font-weight: bold; cursor: pointer; background-color: #17a2b8; color: white; border: 1px solid #107686; border-radius: 4px; font-size: 12px; vertical-align: middle; } .tt-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); z-index: 10020; display: none; justify-content: center; align-items: center; } .tt-modal-content { background: #f8f9fa; padding: 20px; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); width: 90%; max-width: 850px; max-height: 90vh; display: flex; flex-direction: column; } .tt-modal-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #dee2e6; padding-bottom: 10px; margin-bottom: 15px; } .tt-modal-header h3 { margin: 0; font-size: 18px; color: #00529B;} .tt-modal-close { font-size: 24px; font-weight: bold; cursor: pointer; color: #888; } .tt-modal-body { overflow-y: auto; flex-grow: 1; } .tt-tab-container { display: flex; border-bottom: 1px solid #dee2e6; margin-bottom: 15px; } .tt-tab { padding: 8px 16px; cursor: pointer; border: 1px solid transparent; border-bottom: none; margin-bottom: -1px; font-weight: bold; color: #00529B; } .tt-tab.active { background-color: #fff; border-color: #dee2e6 #dee2e6 #fff; border-radius: 4px 4px 0 0; } .tt-tab-content { display: none; } .tt-tab-content.active { display: block; } #tt-chart-container { width: 100%; height: 400px; } .tt-history-table { width: 100%; border-collapse: collapse; font-size: 13px; } .tt-history-table th, .tt-history-table td { border: 1px solid #dee2e6; padding: 8px; text-align: left; } .tt-history-table th { background-color: #e9ecef; } .tt-history-table .status-listed { color: #007bff; } .tt-history-table .status-bid { color: #ff8c00; } .tt-history-table .status-sold { color: #28a745; font-weight: bold; } .tt-history-table .status-expired { color: #dc3545; } .tt-modal-footer { margin-top: 15px; text-align: right; } `); async function showTransferHistoryModal(pid, playerName) { $('#transfer-tracker-modal').remove(); const allHistory = JSON.parse(await GM_getValue(TRANSFER_HISTORY_KEY, '{}')); const playerHistory = allHistory[pid] || []; let tableRows = ''; const chartData = []; let soldEntry = null; if (playerHistory.length > 0) { playerHistory.forEach(entry => { const statusClass = `status-${entry.status.toLowerCase()}`; const statusText = getText(`status${entry.status.charAt(0).toUpperCase() + entry.status.slice(1)}`) || entry.status; const priceNum = parseInt((entry.askingPrice || '0').replace(/\D/g, ''), 10); const bidNum = parseInt((entry.finalBid || '0').replace(/\D/g, ''), 10); tableRows = ` <tr> <td>${new Date(entry.timestamp).toLocaleString()}</td> <td class="${statusClass}">${statusText}</td> <td>${entry.askingPrice || '-'}</td> <td>${entry.finalBid || '-'}</td> <td>${entry.deadline || '-'}</td> <td>${entry.buyer || '-'}</td> </tr>` + tableRows; if (priceNum > 0) { chartData.push({ x: entry.timestamp, y: priceNum, name: `İstenen: ${entry.askingPrice}` }); } if (bidNum > 0) { chartData.push({ x: entry.timestamp, y: bidNum, name: `Teklif: ${entry.finalBid}` }); } if (entry.status === 'sold') { soldEntry = { x: entry.timestamp, y: bidNum, name: `Satıldı: ${entry.finalBid}` }; } }); } else { tableRows = `<tr><td colspan="6" style="text-align:center;">${getText('noHistory')}</td></tr>`; } chartData.sort((a, b) => a.x - b.x); const modalHTML = ` <div id="transfer-tracker-modal" class="tt-modal-overlay"> <div class="tt-modal-content"> <div class="tt-modal-header"> <h3>${getText('historyFor')} ${playerName}</h3> <span class="tt-modal-close">×</span> </div> <div class="tt-tab-container"> <div class="tt-tab active" data-tab="table">Tablo</div> <div class="tt-tab" data-tab="chart">Grafik</div> </div> <div class="tt-modal-body"> <div id="tt-content-table" class="tt-tab-content active"> <table class="tt-history-table"> <thead> <tr> <th>${getText('date')}</th> <th>${getText('status')}</th> <th>${getText('price')}</th> <th>${getText('bid')}</th> <th>${getText('deadline')}</th> <th>${getText('buyer')}</th> </tr> </thead> <tbody>${tableRows}</tbody> </table> </div> <div id="tt-content-chart" class="tt-tab-content"> <div id="tt-chart-container"></div> </div> </div> <div class="tt-modal-footer"> <button id="tt-clear-history" class="mz-script-button" style="background-color: #dc3545;">${getText('clearHistory')}</button> </div> </div> </div>`; $('body').append(modalHTML); $('.tt-tab').on('click', function () { const tabId = $(this).data('tab'); $('.tt-tab').removeClass('active'); $(this).addClass('active'); $('.tt-tab-content').removeClass('active'); $('#tt-content-' + tabId).addClass('active'); }); if (playerHistory.length > 0) { unsafeWindow.Highcharts.chart('tt-chart-container', { chart: { type: 'line' }, title: { text: `${playerName} - Fiyat Değişimi` }, xAxis: { type: 'datetime', title: { text: 'Tarih' } }, yAxis: { title: { text: 'Fiyat (EUR)' }, labels: { formatter: function () { return this.value.toLocaleString('de-DE') + ' EUR'; } }, min: 0 }, tooltip: { formatter: function () { return `<b>${this.point.name}</b><br/>${new Date(this.x).toLocaleString()}`; } }, series: [{ name: 'Fiyat Geçmişi', data: chartData, step: 'left', marker: { enabled: true, radius: 4 } }, ...(soldEntry ? [{ type: 'scatter', name: 'Satış Noktası', data: [soldEntry], marker: { symbol: 'diamond', fillColor: '#28a745', radius: 7, lineWidth: 2, lineColor: '#FFF' } }] : [])] }); } else { $('#tt-chart-container').html(`<div style="text-align:center; padding-top: 50px;">${getText('noHistory')}</div>`); } const modal = $('#transfer-tracker-modal'); modal.css('display', 'flex'); modal.find('.tt-modal-close').on('click', () => modal.remove()); modal.on('click', (e) => { if (e.target === modal[0]) modal.remove(); }); $('#tt-clear-history').on('click', async () => { if (confirm(getText('confirmClear'))) { const currentHistory = JSON.parse(await GM_getValue(TRANSFER_HISTORY_KEY, '{}')); delete currentHistory[pid]; await GM_setValue(TRANSFER_HISTORY_KEY, JSON.stringify(currentHistory)); modal.remove(); } }); } function init() { $('.playerContainer').each(function () { const pid = $(this).find('span.player_id_span').text().trim(); const playerName = $(this).find('span.player_name').text().trim(); if (pid) { const trackBtn = $(`<button class="transfer-track-btn" title="${getText('trackPlayer')}">T</button>`); trackBtn.data('pid', pid).data('player-name', playerName); $(this).find('h2.subheader .floatRight').append(trackBtn); } }); $(document).on('click', '.transfer-track-btn', function () { const pid = $(this).data('pid'); const playerName = $(this).data('player-name'); showTransferHistoryModal(pid, playerName); }); } init(); } /**************************************************************************************** * * * BÖLÜM 7: İSTATİSTİK SAYFASI ÖZETİ (Statistics Summary) - v3.2 (Final Düzeltme) * * * ****************************************************************************************/ function initializeStatisticsSummaryScript() { 'use strict'; const $ = jQuery; // --- Dil Desteği (i18n) --- const i18n = { tr: { summarizeSeasons: "Bu Sekmeyi Özetle", summarizeAll: "TÜM SEKMELERİ ÖZETLE", clearCache: "Sıfırla", // Vurgulanan Değişiklikler BAŞLANGIÇ updateCurrentTab: "Tüm Son Sezonları Güncelle", // <<< YENİ METİN (Bu butonu kullanacağız) tabCacheCleared: (tabName) => `${tabName} sekmesi son sezon önbelleği temizlendi. Güncelleniyor...`, lastSeasonUpdateComplete: "TÜM sekmelerin en son sezonları güncellendi!", // <<< YENİ METİN // Vurgulanan Değişiklikler SON summaryTitle: "Sezon Performansı Özeti", processing: "İşleniyor...", fetchingSeason: (season, current, total) => `${season}. Sezon verileri alınıyor... (${current}/${total})`, processingTab: (tabName, current, total) => `Sekme işleniyor: ${tabName} (${current}/${total})`, summaryComplete: "Tamamlandı! Veriler kaydedildi.", generatingReport: "Rapor oluşturuluyor...", close: "Kapat", season: "Sezon", league: "Lig", rank: "Sıra", points: "Puan", goalsFor: "A.Gol", goalsAgainst: "Y.Gol", avgGoalsFor: "Ort.A", avgGoalsAgainst: "Ort.Y", chartTitle: (metric) => `Sezonlara Göre ${metric} Değişimi`, yAxisTitleLeague: "Lig Seviyesi (Düşük = İyi)", average: "Ortalama", errorActiveTab: "Aktif istatistik sekmesi bulunamadı!", errorTeamId: "Takım ID'si bulunamadı.", errorNoSeasons: "Bu sekmede özetlenecek sezon bulunamadı.", cacheCleared: "Önbellek temizlendi. Veriler baştan taranacak.", }, en: { summarizeSeasons: "Summarize This Tab", summarizeAll: "SUMMARIZE ALL TABS", clearCache: "Reset", // Vurgulanan Değişiklikler BAŞLANGIÇ updateCurrentTab: "Update All Last Seasons", // <<< NEW TEXT (This button will be used) tabCacheCleared: (tabName) => `Last season cache for ${tabName} tab cleared. Updating...`, lastSeasonUpdateComplete: "All sections' last seasons updated!", // <<< NEW TEXT // Vurgulanan Değişiklikler SON summaryTitle: "Season Performance Summary", processing: "Processing...", fetchingSeason: (season, current, total) => `Fetching season ${season}... (${current}/${total})`, processingTab: (tabName, current, total) => `Processing tab: ${tabName} (${current}/${total})`, summaryComplete: "Complete! Data saved.", generatingReport: "Generating report...", close: "Close", season: "Season", league: "League", rank: "Rank", points: "Points", goalsFor: "GF", goalsAgainst: "GA", avgGoalsFor: "Avg. GF", avgGoalsAgainst: "Avg. GA", chartTitle: (metric) => `${metric} History by Season`, yAxisTitleLeague: "League Level (Lower = Better)", average: "Average", errorActiveTab: "Could not find the active statistics tab!", errorTeamId: "Could not find Team ID.", errorNoSeasons: "No seasons found to summarize in this tab.", cacheCleared: "Cache cleared. Data will be rescanned.", } }; const lang = Core.i18n.lang; const getText = (key, ...args) => { const text = i18n[lang][key] || i18n['en'][key]; return typeof text === 'function' ? text(...args) : text; }; // --- Stil Tanımlamaları --- function injectStyles() { GM_addStyle(` #stats-summary-wrapper { display: flex; gap: 10px; align-items: center; padding: 10px 0; border-bottom: 1px solid #ddd; margin-bottom: 10px; flex-wrap: wrap; } #stats-summary-btn, #stats-summary-all-btn, #stats-clear-cache-btn, #stats-update-tab-btn { vertical-align: middle; } /* Genel Mat Yeşil Stil (Yeni eklenen butonlar için) */ .mz-mat-green-btn { background-color: #629c62 !important; background-image: linear-gradient(to bottom, #72a872, #629c62) !important; border-color: #538b53 !important; color: #fff !important; } .mz-mat-green-btn:hover { background-color: #538b53 !important; } /* KIRMIZI SIFIRLA Butonu DÜZELTME (Mevcut kırmızı stili korur) */ #stats-clear-cache-btn .buttonClassMiddle { background-color: #dc3545 !important; background-image: linear-gradient(to bottom, #e35a57, #dc3545) !important; border-color: #c82333 !important; color: white !important; } #stats-clear-cache-btn:hover .buttonClassMiddle { background-color: #c82333 !important; } .stats-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 10050; display: none; justify-content: center; align-items: center; } .stats-modal-content { background: #f4f4f4; padding: 20px; border-radius: 8px; width: 90%; max-width: 1000px; max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 5px 20px rgba(0,0,0,0.4); } .stats-modal-header { border-bottom: 2px solid #ccc; padding-bottom: 10px; margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center; } .stats-modal-header h3 { margin: 0; font-size: 20px; color: #333; } .stats-modal-close { font-size: 28px; font-weight: bold; cursor: pointer; color: #888; } .stats-modal-body { overflow-y: auto; padding-right: 10px; } .stats-summary-table { width: 100%; border-collapse: collapse; font-size: 13px; } .stats-summary-table th, .stats-summary-table td { border: 1px solid #ddd; padding: 8px; text-align: center; } .stats-summary-table th { background-color: #e9ecef; position: sticky; top: 0; z-index: 1; } .stats-summary-table tbody tr:nth-child(odd) { background-color: #f9f9f9; } .stats-summary-table th.chartable-header { cursor: pointer; transition: background-color 0.2s; } .stats-summary-table th.chartable-header:hover { background-color: #dcdcdc; } .stats-summary-table th.active-metric { background-color: #007bff !important; color: white !important; } #stats-history-chart-container { width: 100%; height: 400px; margin-top: 20px; border-top: 2px solid #ccc; padding-top: 20px; } #mz-toast-notification { position: fixed; bottom: 20px; right: 20px; background-color: #28a745; color: white; padding: 12px 20px; border-radius: 5px; z-index: 10099; box-shadow: 0 4px 12px rgba(0,0,0,0.2); opacity: 0; transform: translateY(20px); transition: opacity 0.4s ease, transform 0.4s ease; font-size: 14px; font-weight: bold; } #mz-toast-notification.visible { opacity: 1; transform: translateY(0); } `); } function showToastNotification(message, duration = 3500) { $('#mz-toast-notification').remove(); const toast = $(`<div id="mz-toast-notification"></div>`).text(message); $('body').append(toast); setTimeout(() => { toast.addClass('visible'); }, 10); setTimeout(() => { toast.removeClass('visible'); setTimeout(() => { toast.remove(); }, 500); }, duration); } // --- Ana Fonksiyonlar --- function addSummaryButton() { const targetContainer = $('.statsTabs .ui-tabs-nav').first(); if (targetContainer.length > 0 && $('#stats-summary-wrapper').length === 0) { // Vurgulanan Değişiklikler BAŞLANGIÇ: YENİ BUTONLAR (Mat Yeşil Renk Düzeltmesi) const MAT_YESIL_CLASS = "mz-mat-green-btn"; // Yeni tanımladığımız sınıf // 1. YENİ BUTON: "Tüm Son Sezonları Güncelle" (Mat Yeşil) const updateAllLastSeasonsBtn = $(`<a href="#" id="stats-update-tab-btn" class="mzbtn buttondiv button_account ${MAT_YESIL_CLASS}"> <span class="buttonClassMiddle"><span>${getText('updateCurrentTab')}</span></span> <span class="buttonClassRight"> </span> </a>`); // 2. MEVCUT BUTON (Bu Sekmeyi Özetle): Mat Yeşil const summarizeCurrentBtn = $(`<a href="#" id="stats-summary-btn" class="mzbtn buttondiv button_account ${MAT_YESIL_CLASS}"> <span class="buttonClassMiddle"><span>${getText('summarizeSeasons')}</span></span> <span class="buttonClassRight"> </span> </a>`); // 3. MEVCUT BUTON (TÜM SEKMELERİ ÖZETLE): Mat Yeşil const summaryAllBtn = $(`<a href="#" id="stats-summary-all-btn" class="mzbtn buttondiv button_account ${MAT_YESIL_CLASS}"> <span class="buttonClassMiddle"><span>${getText('summarizeAll')}</span></span> <span class="buttonClassRight"> </span> </a>`); // 4. MEVCUT BUTON (Sıfırla): Kırmızı Renk (Stili CSS'ten geliyor) const clearCacheBtn = $(`<a href="#" id="stats-clear-cache-btn" class="mzbtn buttondiv button_account" title="Tüm verileri sil ve baştan tara"> <span class="buttonClassMiddle"><span>${getText('clearCache')}</span></span> <span class="buttonClassRight"> </span> </a>`); // Event Atamaları summarizeCurrentBtn.on('click', handleSummaryClick); summaryAllBtn.on('click', handleSummaryAllClick); // 👇 SIFIRLA BUTONUNUN EVENT'İ BURADA. KODUNUZDA ZATEN VARDI VE BU KISIM DÜZELTME GEREKİYORSA O BURADADIR clearCacheBtn.on('click', handleClearCacheClick); // 👆 DÜZELTME SONU updateAllLastSeasonsBtn.on('click', handleUpdateAllLastSeasonsClick); // DÜĞME SIRALAMASI: Görseldeki sırayı koruyacak şekilde ekliyoruz: const buttonWrapper = $('<div id="stats-summary-wrapper"></div>') .append(updateAllLastSeasonsBtn) .append(summarizeCurrentBtn) .append(summaryAllBtn) .append(clearCacheBtn); targetContainer.after(buttonWrapper); } } async function handleClearCacheClick(e) { e.preventDefault(); console.log("[MZone Advanced] 'Sıfırla' butonu tıklandı. Önbellek temizleme başlıyor..."); // 1. ADIM: Güvenilir Team ID Bulma let teamId = new URLSearchParams(window.location.search).get('tid'); // Eğer URL'de yoksa, AJAX sekmelerinden birinden almaya çalış if (!teamId) { const allMainTabs = $('.statsTabs > ul.ui-tabs-nav > li > a'); try { const firstAjaxTab = allMainTabs.filter(':not([href^="#"])').first(); if (firstAjaxTab.length > 0) { teamId = new URLSearchParams(firstAjaxTab.attr('href').split('?')[1]).get('tid'); } } catch (error) { console.error("[MZone Advanced] Team ID alınırken hata:", error); } } if (!teamId) { alert(getText('errorTeamId') + " (Sıfırlama işlemi için Team ID bulunamadı.)"); console.error("[MZone Advanced] HATA: Sıfırlama için Team ID bulunamadı."); return; } // 2. ADIM: Kullanıcı Onayı if (!confirm("Bu takımın (ID: " + teamId + ") kayıtlı tüm Sezon Özet verileri kalıcı olarak silinecek ve tekrar tarama yapmanız gerekecek. Emin misiniz?")) { console.log("[MZone Advanced] Sıfırlama işlemi kullanıcı tarafından iptal edildi."); return; } // 3. ADIM: ÖnBelleği Silme const CACHE_KEY = 'mz_stats_summary_cache_v2'; try { const allCaches = JSON.parse(await GM_getValue(CACHE_KEY, '{}')); if (allCaches[teamId]) { delete allCaches[teamId]; await GM_setValue(CACHE_KEY, JSON.stringify(allCaches)); showToastNotification(getText('cacheCleared')); console.log(`[MZone Advanced] BAŞARILI: Takım ID ${teamId} için önbellek temizlendi.`); } else { showToastNotification("Önbellekte bu takıma ait veri bulunamadı."); console.warn(`[MZone Advanced] UYARI: Takım ID ${teamId} için önbellekte veri bulunamadı.`); } } catch (error) { alert("Önbellek silinirken kritik bir hata oluştu. Konsolu kontrol edin."); console.error("[MZone Advanced] KRİTİK HATA: Önbellek silinirken hata:", error); } } async function handleSummaryAllClick(e) { e.preventDefault(); const button = $('#stats-summary-all-btn'); const originalText = button.find('.buttonClassMiddle span').text(); button.find('.buttonClassMiddle span').text(getText('processing')).css('pointer-events', 'none'); NProgress.start(); const allMainTabs = $('.statsTabs > ul.ui-tabs-nav > li > a'); let teamId = new URLSearchParams(window.location.search).get('tid'); if (!teamId) { try { const firstAjaxTab = allMainTabs.filter(':not([href^="#"])').first(); if (firstAjaxTab.length > 0) { teamId = new URLSearchParams(firstAjaxTab.attr('href').split('?')[1]).get('tid'); } } catch (error) { console.error(error); } } if (!teamId) { alert(getText('errorTeamId')); NProgress.done(); button.find('.buttonClassMiddle span').text(originalText).css('pointer-events', 'auto'); return; } const CACHE_KEY = 'mz_stats_summary_cache_v2'; const allCaches = JSON.parse(await GM_getValue(CACHE_KEY, '{}')); if (!allCaches[teamId]) allCaches[teamId] = {}; const teamCache = allCaches[teamId]; for (let i = 0; i < allMainTabs.length; i++) { const tabLink = $(allMainTabs[i]); const categoryKey = tabLink.text().trim(); const tabUrl = tabLink.attr('href'); NProgress.set((i + 0.1) / allMainTabs.length); button.find('.buttonClassMiddle span').text(getText('processingTab', categoryKey, i + 1, allMainTabs.length)); let tabHtml; if (tabUrl.startsWith('#')) { tabHtml = $(tabUrl).html(); } else { try { const response = await GM.xmlHttpRequest({ method: "GET", url: tabUrl }); tabHtml = response.responseText; } catch (error) { console.error(`Sekme '${categoryKey}' yüklenemedi:`, error); continue; } } const seasonTabs = $(`<div>${tabHtml}</div>`).find('.leagueStats > ul > li > a, .topScorers > ul > li > a, .topBadBoys > ul > li > a').first().closest('ul').find('a'); if (seasonTabs.length === 0) continue; if (!teamCache[categoryKey]) teamCache[categoryKey] = []; const cachedSeasons = new Map(teamCache[categoryKey].map(d => [d.season, d])); let hasChanges = false; for (let j = 0; j < seasonTabs.length; j++) { const seasonTab = $(seasonTabs[j]); const season = seasonTab.text().trim(); const existingData = cachedSeasons.get(season); // DÜZELTME: Eğer veri daha önce tarandıysa (scanned: true), boş olsa bile tekrar çekme. // Sadece hiç taranmamışsa (undefined) çek. if (!existingData || !existingData.scanned) { try { const seasonUrl = new URL(seasonTab.attr('href'), window.location.origin).href; const response = await GM.xmlHttpRequest({ method: "GET", url: seasonUrl }); const parsedData = { season, ...parseLeagueDataFromHtml(response.responseText) }; cachedSeasons.set(season, parsedData); hasChanges = true; } catch (error) { console.error(`Sezon ${season} ('${categoryKey}') verisi alınamadı:`, error); } } } if (hasChanges) { teamCache[categoryKey] = Array.from(cachedSeasons.values()); } } await GM_setValue(CACHE_KEY, JSON.stringify(allCaches)); NProgress.done(); button.find('.buttonClassMiddle span').text(originalText).css('pointer-events', 'auto'); showToastNotification(getText('summaryComplete')); } // Vurgulanan Değişiklikler BAŞLANGIÇ: Yeni, Tüm Sekmelerin Son Sezonunu Güncelleyen Fonksiyon async function handleUpdateAllLastSeasonsClick(e) { e.preventDefault(); const button = $('#stats-update-tab-btn'); const originalText = button.find('.buttonClassMiddle span').text(); const resetButton = () => button.find('.buttonClassMiddle span').text(originalText).css('pointer-events', 'auto').fadeTo('fast', 1); button.find('.buttonClassMiddle span').text(getText('processing')).css('pointer-events', 'none').fadeTo('fast', 0.7); NProgress.start(); const allMainTabs = $('.statsTabs > ul.ui-tabs-nav > li > a'); // GÜVENİLİR TEAM ID BULMA BAŞLANGICI let teamId = new URLSearchParams(window.location.search).get('tid'); if (!teamId) { try { const firstAjaxTab = allMainTabs.filter(':not([href^="#"])').first(); if (firstAjaxTab.length > 0) { teamId = new URLSearchParams(firstAjaxTab.attr('href').split('?')[1]).get('tid'); } } catch (error) { console.error(error); } } if (!teamId) { alert(getText('errorTeamId')); NProgress.done(); resetButton(); return; } // GÜVENİLİR TEAM ID BULMA SONU const CACHE_KEY = 'mz_stats_summary_cache_v2'; const allCaches = JSON.parse(await GM_getValue(CACHE_KEY, '{}')); if (!allCaches[teamId]) allCaches[teamId] = {}; const teamCache = allCaches[teamId]; let totalTabsToProcess = allMainTabs.length; let tabsProcessed = 0; let hasChanges = false; for (let i = 0; i < allMainTabs.length; i++) { const tabLink = $(allMainTabs[i]); const categoryKey = tabLink.text().trim(); const tabUrl = tabLink.attr('href'); NProgress.set((i + 0.1) / totalTabsToProcess); button.find('.buttonClassMiddle span').text(getText('processingTab', categoryKey, i + 1, totalTabsToProcess)); tabsProcessed++; let tabHtml; if (tabUrl.startsWith('#')) { // Aktif sekme için DOM'dan al const activePanel = $('.statsTabs > .ui-tabs-panel:visible'); if (activePanel.length > 0) { tabHtml = activePanel.html(); } else { continue; // Boş sekme } } else { // Diğer sekmeler için AJAX ile çek try { const response = await GM.xmlHttpRequest({ method: "GET", url: tabUrl }); tabHtml = response.responseText; } catch (error) { console.error(`Sekme '${categoryKey}' yüklenemedi:`, error); continue; } } // 1. Sekmedeki EN SON SEZON bilgisini bul. const seasonTabs = $(`<div>${tabHtml}</div>`).find('.leagueStats > ul > li > a, .topScorers > ul > li > a, .topBadBoys > ul > li > a').first().closest('ul').find('a'); if (seasonTabs.length > 0) { const lastSeasonElement = $(seasonTabs[0]); // En üstteki (en son) sezon const lastSeason = lastSeasonElement.text().trim(); // 2. Sadece bu sezonun önbelleğini temizle (Eğer varsa) if (teamCache[categoryKey]) { const categoryCache = teamCache[categoryKey]; // Sadece son sezonun verisini önbellekten kaldırarak yeniden taramaya zorla const newCache = categoryCache.filter(d => d.season !== lastSeason); teamCache[categoryKey] = newCache; hasChanges = true; // 3. O sezonu hemen çek ve önbelleğe ekle (handleSummaryClick'in içindeki tek sezon mantığı) try { const seasonUrl = new URL(lastSeasonElement.attr('href'), window.location.origin).href; const response = await GM.xmlHttpRequest({ method: "GET", url: seasonUrl }); const parsedData = { season: lastSeason, ...parseLeagueDataFromHtml(response.responseText) }; teamCache[categoryKey].push(parsedData); teamCache[categoryKey].sort((a, b) => parseFloat(b.season) - parseFloat(a.season)); // Sıralamayı koru } catch (error) { console.error(`Sezon ${lastSeason} ('${categoryKey}') verisi alınamadı:`, error); } } } } if (hasChanges) { await GM_setValue(CACHE_KEY, JSON.stringify(allCaches)); } NProgress.done(); resetButton(); showToastNotification(getText('lastSeasonUpdateComplete')); } async function handleSummaryClick(e) { e.preventDefault(); const button = $('#stats-summary-btn'); const originalText = button.find('.buttonClassMiddle span').text(); const resetButton = () => button.find('.buttonClassMiddle span').text(originalText).css('pointer-events', 'auto').fadeTo('fast', 1); button.find('.buttonClassMiddle span').text(getText('processing')).css('pointer-events', 'none').fadeTo('fast', 0.7); NProgress.start(); const activePanel = $('.statsTabs > .ui-tabs-panel:visible'); const activeCategoryKey = $('.statsTabs > .ui-tabs-nav > li.ui-tabs-active > a').text().trim(); if (activePanel.length === 0 || !activeCategoryKey) { alert(getText('errorActiveTab')); NProgress.done(); resetButton(); return; } const seasonTabs = activePanel.find('.leagueStats > ul > li > a, .topScorers > ul > li > a, .topBadBoys > ul > li > a').first().closest('ul').find('a'); if (seasonTabs.length === 0) { alert(getText('errorNoSeasons')); NProgress.done(); resetButton(); return; } let teamId = new URLSearchParams(window.location.search).get('tid'); if (!teamId) { try { teamId = new URLSearchParams(seasonTabs.first().attr('href').split('?')[1]).get('tid'); } catch (error) { } } if (!teamId) { alert(getText('errorTeamId')); NProgress.done(); resetButton(); return; } const CACHE_KEY = 'mz_stats_summary_cache_v2'; const allCaches = JSON.parse(await GM_getValue(CACHE_KEY, '{}')); const teamCache = allCaches[teamId] || {}; const categoryCache = teamCache[activeCategoryKey] || []; const cachedSeasons = new Map(categoryCache.map(d => [d.season, d])); let hasChanges = false; for (let i = 0; i < seasonTabs.length; i++) { const tab = $(seasonTabs[i]); const season = tab.text().trim(); const existingData = cachedSeasons.get(season); NProgress.set((i + 1) / seasonTabs.length); button.find('.buttonClassMiddle span').text(getText('fetchingSeason', season, i + 1, seasonTabs.length)); // DÜZELTME: Sürekli taramayı önlemek için 'scanned' kontrolü eklendi. // Veri boş olsa bile 'scanned: true' ise tekrar çekmez. if (!existingData || !existingData.scanned) { try { const response = await GM.xmlHttpRequest({ method: "GET", url: tab.attr('href') }); const parsedData = { season, ...parseLeagueDataFromHtml(response.responseText) }; cachedSeasons.set(season, parsedData); hasChanges = true; } catch (error) { console.error(`Sezon ${season} verisi alınamadı:`, error); } } } if (hasChanges) { if (!allCaches[teamId]) allCaches[teamId] = {}; allCaches[teamId][activeCategoryKey] = Array.from(cachedSeasons.values()); await GM_setValue(CACHE_KEY, JSON.stringify(allCaches)); } button.find('.buttonClassMiddle span').text(getText('generatingReport')); createAndShowModal(Array.from(cachedSeasons.values()), activeCategoryKey); NProgress.done(); resetButton(); } // Vurgulanan Değişiklikler BAŞLANGIÇ: Yeni Güncelleme Fonksiyonu async function handleUpdateCurrentTabClick(e) { e.preventDefault(); const button = $('#stats-update-tab-btn'); const originalText = button.find('.buttonClassMiddle span').text(); const resetButton = () => button.find('.buttonClassMiddle span').text(originalText).css('pointer-events', 'auto').fadeTo('fast', 1); const activePanel = $('.statsTabs > .ui-tabs-panel:visible'); const activeCategoryKey = $('.statsTabs > .ui-tabs-nav > li.ui-tabs-active > a').text().trim(); const teamId = new URLSearchParams(window.location.search).get('tid'); if (activePanel.length === 0 || !activeCategoryKey) { alert(getText('errorActiveTab')); return; } if (!teamId) { alert(getText('errorTeamId')); return; } // 1. Sekmedeki EN SON SEZON bilgisini bul. const seasonTabs = activePanel.find('.leagueStats > ul > li > a, .topScorers > ul > li > a, .topBadBoys > ul > li > a').first().closest('ul').find('a'); if (seasonTabs.length === 0) { alert(getText('errorNoSeasons')); return; } // En son (en üstteki) sezonu al const lastSeasonElement = $(seasonTabs[0]); const lastSeason = lastSeasonElement.text().trim(); // 2. Sadece bu sezonun önbelleğini temizle const CACHE_KEY = 'mz_stats_summary_cache_v2'; const allCaches = JSON.parse(await GM_getValue(CACHE_KEY, '{}')); if (allCaches[teamId] && allCaches[teamId][activeCategoryKey]) { const categoryCache = allCaches[teamId][activeCategoryKey]; // Sadece son sezonun verisini önbellekten kaldır allCaches[teamId][activeCategoryKey] = categoryCache.filter(d => d.season !== lastSeason); await GM_setValue(CACHE_KEY, JSON.stringify(allCaches)); } showToastNotification(getText('tabCacheCleared', activeCategoryKey)); // 3. Güncelleme (handleSummaryClick fonksiyonunu taklit et) button.find('.buttonClassMiddle span').text(getText('processing')).css('pointer-events', 'none').fadeTo('fast', 0.7); // handleSummaryClick'i çağırarak tarama işlemini başlat await handleSummaryClick(e); // İşlem bittikten sonra düğmeyi sıfırla resetButton(); } function parseLeagueDataFromHtml(html) { const data = { scanned: true }; // Varsayılan olarak tarandı işareti ekle const tempDiv = $(`<div>${html}</div>`); const rowIndexToDataKeyMap = { 0: 'league', 1: 'points', 2: 'rank', 3: 'goalsFor', 4: 'goalsAgainst', 5: 'avgGoalsFor', 6: 'avgGoalsAgainst' }; const rows = tempDiv.find('.hitlist tbody tr'); if (rows.length > 0) { rows.each(function (index) { const key = rowIndexToDataKeyMap[index]; if (key) { const value = $(this).find('td').eq(1).text().trim(); data[key] = value; } }); } else { // Eğer tablo satırı yoksa (takım oynamamışsa), boş değerler ata data.league = '-'; data.points = '0'; data.rank = '-'; } return data; } function createAndShowModal(data, categoryTitle) { $('#stats-summary-modal').remove(); // Sadece lig verisi olanları göster, boşları grafikten/tablodan hariç tutabiliriz veya "-" gösterebiliriz data.sort((a, b) => parseFloat(b.season) - parseFloat(a.season)); let tableRows = data.map(d => `<tr><td>${d.season}</td><td>${d.league || '-'}</td><td>${d.rank || '-'}</td><td>${d.points || '-'}</td><td>${d.goalsFor || '-'}</td><td>${d.goalsAgainst || '-'}</td><td>${d.avgGoalsFor || '-'}</td><td>${d.avgGoalsAgainst || '-'}</td></tr>`).join(''); const modalHTML = ` <div id="stats-summary-modal" class="stats-modal-overlay"> <div class="stats-modal-content"> <div class="stats-modal-header"><h3>${categoryTitle} - ${getText('summaryTitle')}</h3><span class="stats-modal-close">×</span></div> <div class="stats-modal-body"> <table class="stats-summary-table"> <thead> <tr> <th>${getText('season')}</th> <th class="chartable-header active-metric" data-metric="level">${getText('league')}</th> <th class="chartable-header" data-metric="rank">${getText('rank')}</th> <th class="chartable-header" data-metric="points">${getText('points')}</th> <th class="chartable-header" data-metric="goalsFor">${getText('goalsFor')}</th> <th class="chartable-header" data-metric="goalsAgainst">${getText('goalsAgainst')}</th> <th class="chartable-header" data-metric="avgGoalsFor">${getText('avgGoalsFor')}</th> <th class="chartable-header" data-metric="avgGoalsAgainst">${getText('avgGoalsAgainst')}</th> </tr> </thead> <tbody>${tableRows}</tbody> </table> <div id="stats-history-chart-container"></div> </div> </div> </div>`; $('body').append(modalHTML); renderHistoryChart(data, 'level'); const modal = $('#stats-summary-modal'); modal.css('display', 'flex'); modal.find('.stats-modal-close').on('click', () => modal.remove()); modal.on('click', '.chartable-header', function () { const metric = $(this).data('metric'); modal.find('.chartable-header').removeClass('active-metric'); $(this).addClass('active-metric'); renderHistoryChart(data, metric); }); } function parseLeagueLevel(leagueName) { if (!leagueName || typeof leagueName !== 'string' || leagueName.toLowerCase().includes('hata!') || leagueName === '-') return null; const match = leagueName.toLowerCase().match(/div(\d+)\.(\d+)/); if (match) return parseInt(match[1], 10) + 1; if (/dünya ligi|world league/i.test(leagueName)) return 1; return 1; } function renderHistoryChart(data, metric) { const metricConfig = { level: { key: 'league', yAxisTitle: getText('yAxisTitleLeague'), reversed: true, parser: parseLeagueLevel, name: getText('league') }, rank: { key: 'rank', yAxisTitle: getText('rank'), reversed: true, parser: s => parseFloat(s), name: getText('rank') }, points: { key: 'points', yAxisTitle: getText('points'), reversed: false, parser: s => parseFloat(s), name: getText('points') }, goalsFor: { key: 'goalsFor', yAxisTitle: getText('goalsFor'), reversed: false, parser: s => parseFloat(s), name: getText('goalsFor') }, goalsAgainst: { key: 'goalsAgainst', yAxisTitle: getText('goalsAgainst'), reversed: false, parser: s => parseFloat(s), name: getText('goalsAgainst') }, avgGoalsFor: { key: 'avgGoalsFor', yAxisTitle: getText('avgGoalsFor'), reversed: false, parser: s => parseFloat(s), name: getText('avgGoalsFor') }, avgGoalsAgainst: { key: 'avgGoalsAgainst', yAxisTitle: getText('avgGoalsAgainst'), reversed: false, parser: s => parseFloat(s), name: getText('avgGoalsAgainst') } }; const config = metricConfig[metric]; if (!config) return; const chartData = data .map(d => ({ season: parseFloat(d.season), y: config.parser(d[config.key]), originalValue: d[config.key] })) .filter(d => d.y !== null && !isNaN(d.y)) .sort((a, b) => a.season - b.season); if (chartData.length === 0) { $('#stats-history-chart-container').html(`<div style="text-align:center; padding-top:50px;">Grafik için veri bulunamadı.</div>`); return; } const total = chartData.reduce((sum, point) => sum + point.y, 0); const average = chartData.length > 0 ? total / chartData.length : 0; unsafeWindow.Highcharts.chart('stats-history-chart-container', { chart: { type: 'line' }, title: { text: getText('chartTitle', config.name) }, xAxis: { title: { text: getText('season') }, categories: chartData.map(d => d.season.toString()) }, yAxis: { title: { text: config.yAxisTitle }, reversed: config.reversed, plotLines: [{ value: average, color: '#d9534f', dashStyle: 'shortdash', width: 2, 'zIndex': 5, label: { useHTML: true, formatter: function () { return `<div style="background-color: rgba(217, 83, 79, 0.9); color: white; padding: 3px 6px; border-radius: 4px; font-weight: bold; font-size: 11px; box-shadow: 0 1px 3px rgba(0,0,0,0.4); text-shadow: none;">${getText('average')}: ${average.toFixed(2)}</div>`; }, align: 'right', x: -5, y: -7 } }] }, tooltip: { formatter: function () { const pointData = chartData.find(d => d.season.toString() === this.x); return `<b>${getText('season')} ${this.x}</b><br/>${config.name}: <b>${pointData.originalValue}</b>`; } }, legend: { enabled: false }, series: [{ name: config.name, data: chartData.map(d => d.y), marker: { enabled: true, radius: 4 }, dataLabels: { enabled: true, formatter: function () { const pointData = chartData.find(d => d.season.toString() === this.x); return pointData ? pointData.originalValue : ''; }, style: { fontSize: '10px', fontWeight: 'bold', color: 'gray', textOutline: '1px white' } } }] }); } function init() { if ($('.statsTabs').length) { injectStyles(); addSummaryButton(); } } $(document).ready(init); } /**************************************************************************************** * * * BÖLÜM EK: GHOST ROBOT - DURAN TOP KOPYALAYICI (Çoklu Dil) * * * ****************************************************************************************/ function initializeGhostRobotScript() { 'use strict'; const $ = jQuery; // Global değişkenler (Modül kapsamında) // Global değişkenler (Modül kapsamında) let sourceDataMap = {}; let tacticsQueue = []; let popupKillerInterval = null; const waitForButton = setInterval(() => { const testBtn = $('#test_tactics_button'); const container = testBtn.parent(); if (container.length > 0 && $('#xml_ghost_btn').length === 0) { clearInterval(waitForButton); container.css('position', 'relative'); const isMobile = window.innerWidth <= 900; if (isMobile) { const btn = $(`<a href="#" id="xml_ghost_btn" style="display: flex !important; align-items: center !important; justify-content: center !important; background: #2e7d32 !important; color: white !important; border: 1px solid #1b5e20 !important; border-radius: 4px !important; padding: 6px !important; margin: 8px 0 !important; text-decoration: none !important; font-weight: bold !important; font-size: 12px !important; width: 100% !important; box-sizing: border-box !important;"> <span style="margin-right: 6px;">📋</span> ${getText('ghostBtnTitle')} </a>`); btn.on('click', function (e) { e.preventDefault(); startGhostProcess(); }); container.append(btn); } else { const btn = $(`<a href="#" id="xml_ghost_btn" class="mzbtn buttondiv button_account" style="position: absolute; top: -35px; right: 0px; width: auto; background-color: #1b5e20; border: 1px solid #66bb6a; z-index: 90;"> <span class="buttonClassMiddle" style="color: white; font-weight: bold;"> <span style="white-space: nowrap">${getText('ghostBtnTitle')}</span> </span> <span class="buttonClassRight"> </span> </a>`); btn.on('click', function (e) { e.preventDefault(); startGhostProcess(); }); container.append(btn); } console.log(getText('ghostLogReady')); } }, 1000); function startPopupKiller() { if (popupKillerInterval) clearInterval(popupKillerInterval); popupKillerInterval = setInterval(() => { const powerboxBtn = $('#powerbox_confirm_ok_button'); if (powerboxBtn.length > 0 && powerboxBtn.is(':visible')) { powerboxBtn[0].click(); } const dialogs = $('.ui-dialog:visible'); dialogs.each(function () { const text = $(this).text(); // Evrensel kontrol: Türkçe, İngilizce veya genel "Tamam/OK" if (text.includes("başarı ile eklendi") || text.includes("successfully added") || text.includes("Tamam") || text.includes("OK")) { const okBtn = $(this).find('.ui-dialog-buttonpane button'); if (okBtn.length > 0) okBtn.click(); } }); }, 200); } function stopPopupKiller() { if (popupKillerInterval) clearInterval(popupKillerInterval); } async function startGhostProcess() { try { console.clear(); console.log(getText('ghostLogStart')); const editor = unsafeWindow.teamTactic.editor; const textArea = $('#importExportData'); $('#importExportTacticsWindow').css({ 'opacity': '0.01', 'pointer-events': 'none' }); if (!$('#importExportTacticsWindow').is(':visible')) { unsafeWindow.toggleImportExportWindow(); } textArea.val(''); editor.exportTactic(); await wait(300); const sourceXML = textArea.val(); if (!sourceXML) { alert(getText('ghostErrorNoXML')); restoreUI(); return; } sourceDataMap = parseXMLToMap(sourceXML); if (Object.keys(sourceDataMap).length === 0) { alert(getText('ghostErrorNoPlayer')); restoreUI(); return; } const mzData = unsafeWindow.teamTactic.tacticsData; let allTactics = []; if (mzData.TeamTactics && mzData.TeamTactics.Tactic) { allTactics = mzData.TeamTactics.Tactic; } else if (mzData.tactics && mzData.tactics.TeamTactics) { allTactics = mzData.tactics.TeamTactics.Tactic; } if (!Array.isArray(allTactics)) allTactics = [allTactics]; const currentTabId = $('#current_tab').val(); const currentTabName = $(`#tacticTab_${currentTabId} span`).text(); tacticsQueue = allTactics .filter(t => t['@attributes'].name !== currentTabId && t.TacticPlayer) .map(t => t['@attributes'].name); if (tacticsQueue.length === 0) { alert(getText('ghostErrorNoTarget')); restoreUI(); return; } if (!confirm(getText('ghostConfirmMsg', currentTabName, tacticsQueue.length))) { restoreUI(); return; } startPopupKiller(); processNextTactic(); } catch (err) { console.error(err); alert("Error: " + err.message); stopPopupKiller(); restoreUI(); } } async function processNextTactic() { if (tacticsQueue.length === 0) { finishProcess(); return; } const tabId = tacticsQueue.shift(); try { const textArea = $('#importExportData'); const editor = unsafeWindow.teamTactic.editor; unsafeWindow.changeTacticTab(tabId); await wait(700); textArea.val(''); editor.exportTactic(); await wait(200); let targetXML = textArea.val(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(targetXML, "text/xml"); const positions = []; const posNodes = xmlDoc.getElementsByTagName("Pos"); const subNodes = xmlDoc.getElementsByTagName("Sub"); for (let i = 0; i < posNodes.length; i++) positions.push(posNodes[i]); for (let i = 0; i < subNodes.length; i++) positions.push(subNodes[i]); let hasChanges = false; for (let i = 0; i < positions.length; i++) { const node = positions[i]; const pid = node.getAttribute("pid"); if (sourceDataMap[pid]) { const sourceInfo = sourceDataMap[pid]; if (sourceInfo.pt && node.getAttribute("pt") !== sourceInfo.pt) { node.setAttribute("pt", sourceInfo.pt); hasChanges = true; } if (sourceInfo.fk && node.getAttribute("fk") !== sourceInfo.fk) { node.setAttribute("fk", sourceInfo.fk); hasChanges = true; } } } if (hasChanges) { const serializer = new XMLSerializer(); const modifiedXML = serializer.serializeToString(xmlDoc); textArea.val(modifiedXML); editor.importTactic(); await wait(600); } processNextTactic(); } catch (err) { console.error(`Tactic ${tabId} error:`, err); processNextTactic(); } } function finishProcess() { stopPopupKiller(); restoreUI(); unsafeWindow.toggleImportExportWindow(); $('#save_all').val('1'); alert(getText('ghostSuccessMsg')); const saveBtn = $('#saveallb'); $('html, body').animate({ scrollTop: saveBtn.offset().top }, 500); saveBtn.css({ 'border': '5px solid red', 'transform': 'scale(1.2)', 'transition': '0.5s' }); } function restoreUI() { $('#importExportTacticsWindow').css({ 'opacity': '1', 'pointer-events': 'auto' }); } function parseXMLToMap(xmlString) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, "text/xml"); const map = {}; const positions = []; const posNodes = xmlDoc.getElementsByTagName("Pos"); const subNodes = xmlDoc.getElementsByTagName("Sub"); for (let i = 0; i < posNodes.length; i++) positions.push(posNodes[i]); for (let i = 0; i < subNodes.length; i++) positions.push(subNodes[i]); for (let i = 0; i < positions.length; i++) { const node = positions[i]; const pid = node.getAttribute("pid"); const pt = node.getAttribute("pt"); const fk = node.getAttribute("fk"); if (pid) { map[pid] = {}; if (pt) map[pid].pt = pt; if (fk) map[pid].fk = fk; } } return map; } function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } /**************************************************************************************** * * * BÖLÜM 8: GENEL BİLGİ MENÜSÜ ve AYARLAR MODALI (GÖRÜNÜM DÜZELTİLDİ - FINAL + MOBİL) * * * ****************************************************************************************/ function initializeGlobalInfoMenu() { 'use strict'; const lang = Core.i18n.lang; // Menü Linkleri const featurePages = [ { url: "/?p=league", titleKey: "leagueTitle", descKey: "leagueDesc" }, { url: "/?p=statistics", titleKey: "statisticsTitle", descKey: "statisticsDesc" }, { url: "/?p=shortlist", titleKey: "shortlistTitle", descKey: "shortlistDesc" }, { url: "/?p=players", titleKey: "playersTitle", descKey: "playersDesc" }, // 👇 YENİ MODÜL LİNKİ BURADA { url: "/?p=challenges", titleKey: "friendlyMatchAutoTitle", descKey: "friendlyMatchAutoDesc" }, // 👇 DİĞER LİNKLER { url: "/?p=match&sub=scheduled", titleKey: "fixtureToolsTitle", descKey: "fixtureToolsDesc" }, { url: "/?p=match&sub=played", titleKey: "resultsToolsTitle", descKey: "resultsToolsDesc" }, { url: "/?p=rank&sub=userrank", titleKey: "rankingTweaksTitle", descKey: "rankingTweaksDesc" }, { url: "/?p=tactics", titleKey: "ghostRobotTitle", descKey: "ghostRobotDesc" }, { url: "/?p=league", titleKey: "linkEnhancementsTitle", descKey: "linkEnhancementsDesc" } ]; const POSITION_KEY = 'mz_global_info_btn_position'; // --- Gerekli Stiller (CSS) - DÜZELTİLDİ + MOBİL KÜÇÜLTME --- GM_addStyle(` /* Global Info Panel Footer */ .mz-gip-footer { padding: 10px; background-color: #f1f3f5; border-top: 1px solid #dee2e6; text-align: center; border-radius: 0 0 8px 8px; flex-shrink: 0; } .mz-global-reset-btn { background-color: #dc3545; color: white; border: none; padding: 8px 15px; border-radius: 4px; font-size: 13px; cursor: pointer; font-weight: bold; transition: background-color 0.2s, transform 0.1s; width: 100%; box-sizing: border-box; box-shadow: 0 2px 4px rgba(220, 53, 69, 0.3); } .mz-global-reset-btn:hover { background-color: #c82333; transform: translateY(-1px); } .mz-global-reset-btn:active { transform: translateY(0); } /* Ana Buton */ #mz-global-info-btn { position: fixed; top: calc(50% - 20px); left: 2px; bottom: auto; width: 45px; height: 45px; background-color: #00529B; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: bold; font-family: 'Georgia', serif; cursor: grab; box-shadow: 0 4px 10px rgba(0,0,0,0.3); z-index: 999999; transition: transform 0.2s ease-in-out, background-color 0.2s, width 0.3s, left 0.3s, opacity 0.3s; border: 2px solid white; user-select: none; } #mz-global-info-btn:hover { transform: scale(1.1); background-color: #0066c2; } #mz-global-info-btn.is-dragging { cursor: grabbing !important; transform: scale(1.15); box-shadow: 0 8px 20px rgba(0,0,0,0.4); } /* KÜÇÜLTME BUTONU VE STİLLERİ (Masaüstü ve Mobil) */ #mz-mobile-toggle-btn { display: block; position: absolute; top: -5px; left: -5px; width: 20px; height: 20px; background: #ffc107; color: #000; border-radius: 50%; font-size: 16px; line-height: 18px; text-align: center; cursor: pointer; z-index: 9999; border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.4); font-weight: bold; } /* Küçültülmüş Hal - Sol kenara yapışık */ #mz-global-info-btn.mz-minimized { width: 30px !important; height: 60px !important; left: 0 !important; right: auto !important; top: calc(50% - 20px) !important; bottom: auto !important; border-radius: 0 10px 10px 0 !important; opacity: 0.85; font-size: 0 !important; background-color: #00529B !important; transform: none !important; cursor: pointer; transition: opacity 0.3s, background-color 0.3s, transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); border: 2px solid white; border-left: none; } @media (max-width: 900px) { #mz-global-info-btn.mz-minimized { width: 24px !important; height: 52px !important; opacity: 0.75; } #mz-global-info-btn.mz-minimized::after { line-height: 52px !important; font-size: 16px !important; } } #mz-global-info-btn.mz-minimized:hover { opacity: 1 !important; background-color: #003d73 !important; } /* Küçültülmüş haldeyken toggle butonu */ #mz-global-info-btn.mz-minimized #mz-mobile-toggle-btn { display: none; /* Küçüldüğünde toggle butonunu gizle, ana butona tıklacak açacak */ } /* Küçültülmüş haldeyken içine ok işareti koy */ #mz-global-info-btn.mz-minimized::after { content: '»'; font-size: 20px; color: white; line-height: 60px; margin-right: 2px; font-weight: bold; } #mz-global-info-panel { position: fixed; width: 340px; max-width: 90vw; left: 0; top: 0; transform: translateY(8px); background: rgba(248, 249, 250, 0.95); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.4); z-index: 1000000; opacity: 0; transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); pointer-events: none; font-family: Dosis, sans-serif; display: flex; flex-direction: column; overflow: hidden; max-height: 85vh; } #mz-global-info-panel.visible { opacity: 1; transform: translateY(0); pointer-events: auto; } /* Header */ .mz-gip-header { padding: 15px; font-size: 16px; font-weight: bold; color: #fff; background-color: #343a40; border-radius: 8px 8px 0 0; border-bottom: 1px solid #4a4a4a; position: relative; flex-shrink: 0; } /* Liste (Scroll Sorunu Düzeltildi) */ .mz-gip-list { list-style: none; padding: 10px; margin: 0; overflow-y: auto; flex-grow: 1; } .mz-gip-list li { margin-bottom: 4px; } .mz-gip-list a { display: block; padding: 10px 12px; text-decoration: none; color: #212529; border-radius: 5px; transition: background-color 0.2s, color 0.2s; border: 1px solid transparent; } .mz-gip-list a:hover { background-color: #e9ecef; color: #0056b3; border-color: #dee2e6; } /* Yazı Tipleri */ .mz-gip-list strong { font-size: 14px; color: #00529B; display: block; margin-bottom: 3px; } .mz-gip-list p { font-size: 12px; margin: 0; color: #6c757d; line-height: 1.35; } /* Yazar Linki */ .author-link { text-decoration: none; cursor: pointer; color: inherit; transition: background-color 0.2s, color 0.2s; padding: 2px 4px; border-radius: 3px; margin: -2px -4px; } .author-link:hover { text-decoration: none !important; background-color: #ffc107; color: #212529 !important; } /* Ayarlar Modalı Stilleri */ #mz-feature-settings-modal { display: none; position: fixed; z-index: 100000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); justify-content: center; align-items: center; } .mz-fs-content { background: #f8f9fa; padding: 20px; border-radius: 8px; width: 90%; max-width: 550px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); } .mz-fs-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #dee2e6; padding-bottom: 10px; margin-bottom: 15px; } .mz-fs-header h3 { margin: 0; font-size: 18px; } .mz-fs-close { font-size: 24px; font-weight: bold; cursor: pointer; color: #888; } .mz-fs-controls { display: flex; justify-content: space-between; align-items: center; padding-bottom: 10px; margin-bottom: 10px; border-bottom: 1px solid #eee; } .mz-fs-controls a { font-size: 12px; color: #007bff; text-decoration: none; font-weight: bold; } .mz-fs-controls a:hover { text-decoration: underline; } .mz-fs-body { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .mz-fs-label { display: flex; align-items: center; gap: 8px; background: #f0f0f0; padding: 8px; border-radius: 4px; cursor: pointer; border: 1px solid #ddd; } .mz-fs-footer { text-align: center; border-top: 1px solid #dee2e6; padding-top: 15px; margin-top: 15px; } `); // --- Fonksiyonlar --- function createFeatureSettingsModal() { if ($('#mz-feature-settings-modal').length > 0) return; let checkboxesHTML = ''; for (const key in MODULES) { let rawName = getText(MODULES[key].nameKey); if (!rawName) { console.warn(`[MZone] Çeviri bulunamadı: ${MODULES[key].nameKey}`); rawName = `[${MODULES[key].nameKey}]`; } const moduleName = rawName.replace(/<[^>]*>?/gm, ''); checkboxesHTML += `<label class="mz-fs-label"><input type="checkbox" id="setting-${key}" data-key="${key}"><span>${moduleName}</span></label>`; } const modalHTML = `<div id="mz-feature-settings-modal"><div class="mz-fs-content"><div class="mz-fs-header"><h3>${getText('featureSettingsTitle')}</h3><span class="mz-fs-close">×</span></div><div class="mz-fs-controls"><a href="#" id="mz-fs-select-all">${getText('selectAll')}</a><a href="#" id="mz-fs-deselect-all">${getText('deselectAll')}</a></div><div class="mz-fs-body">${checkboxesHTML}</div><div class="mz-fs-footer"><button id="mz-save-feature-settings" class="mz-script-button" style="background-color: #28a745;">${getText('saveSettings')}</button><p style="font-size: 11px; color: #6c757d; margin-top: 10px;">${getText('reloadToApply')}</p></div></div></div>`; const featureSettings = $(modalHTML); window.__mzcMega.mountDonorContent("mzone-advanced", featureSettings[0], "MZone Advanced"); $('#mz-feature-settings-modal .mz-fs-close').on('click', () => $('#mz-feature-settings-modal').hide()); $('#mz-save-feature-settings').on('click', saveAndCloseFeatureSettings); $('#mz-fs-select-all').on('click', (e) => { e.preventDefault(); $('#mz-feature-settings-modal .mz-fs-body input[type="checkbox"]').prop('checked', true); }); $('#mz-fs-deselect-all').on('click', (e) => { e.preventDefault(); $('#mz-feature-settings-modal .mz-fs-body input[type="checkbox"]').prop('checked', false); }); } async function openFeatureSettingsModal() { const modal = $('#mz-feature-settings-modal'); for (const key in MODULES) { modal.find(`#setting-${key}`).prop('checked', scriptSettings[key]); } modal.css('display', 'flex'); } async function saveAndCloseFeatureSettings() { for (const key in MODULES) { scriptSettings[key] = $(`#setting-${key}`).is(':checked'); } await GM_setValue(MODULE_SETTINGS_KEY, JSON.stringify(scriptSettings)); $('#mz-feature-settings-modal').hide(); alert('Ayarlar kaydedildi. Değişikliklerin geçerli olması için sayfayı yenilemeniz gerekebilir.'); } async function createMenu() { if ($('#mz-global-info-btn').length > 0) return; // 1. Önce Butonu Oluştur - KONTROL PANELİ VARSAYILAN OLARAK KAPALI GELSİN (Eski Görünüm) const infoButton = $(`<div id="mz-global-info-btn" class="mz-minimized" title="${getText('menuTitle')}\n${getText('dragToMove')}">i<div id="mz-mobile-toggle-btn">-</div></div>`); const infoPanel = $('<div id="mz-global-info-panel"></div>'); // 2. Panel İçeriğini Hazırla const baseTitle = getText('menuTitle'); const scriptAuthor = GM_info.script.author; const hardcodedTeamName = "☆TC☆ NiCoTiN ☆TC☆"; const scriptNameToDisplay = (lang === 'tr') ? "MZone Gelişmiş: Tablo, İstatistik & Play-off" : "MZone Advanced: Table, Stats & Play-off"; const authorLineHtml = `<a href="#" id="author-message-link" class="author-link" title="${getText('sendMessageToAuthor')}">by ${scriptAuthor} (${hardcodedTeamName})</a>`; const fullHeaderHtml = `${baseTitle}<br><small style="font-weight: normal; font-size: 0.75em; opacity: 0.85; line-height: 1.4;">${scriptNameToDisplay}<br>${authorLineHtml}</small>`; const settingsButtonHtml = `<span id="mz-open-feature-settings" title="${getText('settings')}" style="position: absolute; top: 10px; right: 12px; font-size: 20px; cursor: pointer; transition: transform 0.2s;">⚙️</span>`; let listItems = featurePages.map(page => { const urlParams = new URLSearchParams(page.url.split('?')[1]); let moduleKey = ''; if (page.url.includes('p=league')) moduleKey = 'leagueTable'; else if (page.url.includes('p=statistics')) moduleKey = 'statisticsSummary'; else if (page.url.includes('p=shortlist')) moduleKey = 'shortlistFilter'; else if (page.url.includes('p=players')) moduleKey = 'playerStats'; else if (page.url.includes('p=challenges')) moduleKey = 'friendlyMatchAuto'; else if (page.url.includes('p=tactics')) moduleKey = 'ghostRobot'; if (moduleKey && scriptSettings[moduleKey] === false) { return ''; } return `<li><a href="${page.url}"><strong>${getText(page.titleKey)}</strong><p>${getText(page.descKey)}</p></a></li>`; }).join(''); const clearBtnText = getText('clearTrainingCache') || "🗑️ Tüm Önbelleği Temizle"; const clearButtonHtml = `<div class="mz-gip-footer"><button id="mz-global-clear-cache" class="mz-global-reset-btn">${clearBtnText}</button></div>`; infoPanel.html(`<div class="mz-gip-header">${fullHeaderHtml}${settingsButtonHtml}</div><ul class="mz-gip-list">${listItems}</ul>${clearButtonHtml}`); // 3. Konumlandırma try { const savedPosition = JSON.parse(await GM_getValue(POSITION_KEY, 'null')); if (savedPosition && savedPosition.top !== undefined && savedPosition.left !== undefined) { let topVal = parseInt(savedPosition.top); let leftVal = parseInt(savedPosition.left); const winHeight = $(window).height(); const winWidth = $(window).width(); if (!isNaN(topVal) && !isNaN(leftVal) && topVal >= 0 && topVal < winHeight && leftVal >= 0 && leftVal < winWidth) { infoButton.css({ top: savedPosition.top, left: savedPosition.left, bottom: 'auto', right: 'auto' }); } } } catch (e) { console.error("Konum yüklenirken hata oluştu:", e); } window.__mzcMega.mountDonorContent("mzone-advanced", infoPanel[0], "MZone Advanced"); // MOBİL TOGGLE İÇİN EVENT infoButton.find('#mz-mobile-toggle-btn').on('click', function (e) { e.stopPropagation(); // Butonun sürüklenmesini veya menü açmasını engelle $('#mz-global-info-btn').addClass('mz-minimized'); $('#mz-global-info-panel').removeClass('visible'); // Menü açıksa kapat }); // Küçültülmüş butona tıklayınca direkt paneli aç/kapat (iki adım yok) infoButton.on('click', function (e) { if ($(this).hasClass('mz-minimized')) { e.stopPropagation(); positionPanelRelativeToButton(); $('#mz-global-info-panel').toggleClass('visible'); } }); } function makeButtonInteractive() { const button = $('#mz-global-info-btn'); let isDragging = false; let hasMoved = false; let startX, startY, offsetX, offsetY; button.on('mousedown touchstart', function (e) { // Eğer küçültülmüşse sürüklemeyi engelle if (button.hasClass('mz-minimized')) return; const evt = e.type === 'touchstart' ? e.originalEvent.touches[0] : e; if (e.type === 'mousedown' && e.which !== 1) return; // e.preventDefault(); // Touch scrollu engellememesi için mobilde dikkatli kullanılmalı if (e.type === 'mousedown') e.preventDefault(); isDragging = false; hasMoved = false; startX = evt.clientX; startY = evt.clientY; const buttonPos = button.offset(); offsetX = evt.clientX - buttonPos.left; offsetY = evt.clientY - buttonPos.top; if (e.type === 'touchstart') { // Mobilde scroll'u engellememek için dragging durumunu mousedown/touchstart anında true yapmıyoruz. // Sadece touchmove başladığında moveThreshold aşılırsa true yapılacak. $(document).on('touchmove.interactive', onMouseMove); $(document).on('touchend.interactive touchcancel.interactive', onMouseUp); } else { $(document).on('mousemove.interactive', onMouseMove); $(document).on('mouseup.interactive', onMouseUp); } }); function onMouseMove(e) { const evt = e.type === 'touchmove' ? e.originalEvent.touches[0] : e; if (!isDragging) { const moveThreshold = 5; if (Math.abs(evt.clientX - startX) > moveThreshold || Math.abs(evt.clientY - startY) > moveThreshold) { isDragging = true; hasMoved = true; button.addClass('is-dragging'); } } if (isDragging) { if (e.type === 'touchmove') e.preventDefault(); // Sürüklerken sayfa kaymasını engelle updatePosition(evt); } } function updatePosition(e) { let newLeft = e.clientX - offsetX; let newTop = e.clientY - offsetY; const buttonWidth = button.outerWidth(); const buttonHeight = button.outerHeight(); const windowWidth = $(window).width(); const windowHeight = $(window).height(); newLeft = Math.max(0, Math.min(newLeft, windowWidth - buttonWidth)); newTop = Math.max(0, Math.min(newTop, windowHeight - buttonHeight)); button.css({ top: newTop, left: newLeft, bottom: 'auto', right: 'auto' }); } async function onMouseUp(e) { $(document).off('.interactive'); button.removeClass('is-dragging'); if (isDragging) { const finalPosition = { top: button.css('top'), left: button.css('left') }; await GM_setValue(POSITION_KEY, JSON.stringify(finalPosition)); } if (!hasMoved && !button.hasClass('mz-minimized')) { // Eğer buton minimize değilse ve sürüklenmediyse menüyü aç/kapat // Tıklanan yer toggle butonu değilse (zaten yukarıda stopPropagation var ama garanti olsun) if (!$(e.target).is('#mz-mobile-toggle-btn')) { positionPanelRelativeToButton(); $('#mz-global-info-panel').toggleClass('visible'); } } isDragging = false; hasMoved = false; button.removeClass('is-dragging'); } // --- Global Cache Reset Logic --- $(document).on('click', '#mz-global-clear-cache', function (e) { e.preventDefault(); const confirmMsg = (Core.i18n.lang === 'tr') ? "Tüm oyuncuların kayıtlı antrenman verileri silinecek. Emin misiniz?" : "All saved player training data will be deleted. Are you sure?"; if (confirm(confirmMsg)) { if (typeof PlayerDataCache !== 'undefined' && PlayerDataCache.clear) { PlayerDataCache.clear(); const successMsg = (Core.i18n.lang === 'tr') ? "Tüm veriler temizlendi. Sayfa yenileniyor..." : "All data cleared. Reloading page..."; alert(successMsg); location.reload(); } } }); } function positionPanelRelativeToButton() { const button = $('#mz-global-info-btn'); const panel = $('#mz-global-info-panel'); const margin = 10; panel.css({ visibility: 'hidden', display: 'block' }); const panelWidth = panel.outerWidth(); const panelHeight = panel.outerHeight(); panel.css({ visibility: '', display: '' }); const buttonOffset = button.offset(); const buttonWidth = button.outerWidth(); const buttonHeight = button.outerHeight(); const windowWidth = $(window).width(); const windowHeight = $(window).height(); // YATAY KONUMLANDIRMA: Butonun sağından başlasın (Sol Kenardayken) let targetLeft = buttonOffset.left + buttonWidth + margin; // Eğer sağda yer kalmazsa sola aç (Buton sağ kenardaysa) if (targetLeft + panelWidth > windowWidth - margin) { targetLeft = buttonOffset.left - panelWidth - margin; } // DİKEY KONUMLANDIRMA: Butonu dikeyde ortalayalım (Panel çok uzunsa ekran sınırlarına sadık kalalım) let targetTop = buttonOffset.top + (buttonHeight / 2) - (panelHeight / 2); // Ekran sınırlarını kontrol et targetTop = Math.max(margin, Math.min(targetTop, windowHeight - panelHeight - margin)); targetLeft = Math.max(margin, Math.min(targetLeft, windowWidth - panelWidth - margin)); panel.css({ top: `${targetTop}px`, left: `${targetLeft}px`, bottom: 'auto', right: 'auto' }); } // --- Başlatıcı --- $(document).ready(function () { createFeatureSettingsModal(); createMenu().then(() => { // unified mzcopilot toolbar owns positioning $(document).on('click', '#mz-open-feature-settings', openFeatureSettingsModal); }); $(document).on('click', '#author-message-link', function (e) { e.preventDefault(); const tempMessengerLink = $(`<a href="/?p=messenger&uid=3111770" class="messenger-link"></a>`); tempMessengerLink.css('display', 'none').appendTo('body'); tempMessengerLink[0].click(); tempMessengerLink.remove(); }); $(document).on('click', function (e) { const panel = $('#mz-global-info-panel'); const button = $('#mz-global-info-btn'); if (panel.length && panel.hasClass('visible')) { if (!panel.is(e.target) && panel.has(e.target).length === 0 && !button.is(e.target) && button.has(e.target).length === 0) { panel.removeClass('visible'); } } }); }); } /**************************************************************************************** * * * BÖLÜM 9: OYUNCU BECERİ RENKLENDİRME (Van Style Coloring) - NİHAİ SÜRÜM (v8) * * * ****************************************************************************************/ function initializeSkillColoringScript() { 'use strict'; const $ = jQuery; const pParam = new URLSearchParams(window.location.search).get('p'); if (pParam === 'shortlist') return; console.log('[MZ Skill Coloring] Modül Başlatıldı (Nihai Sürüm v8 - Panel Durumu Kayıtlı).'); // Panel durumunu kaydetmek için helper fonksiyon const PANEL_STATE_KEY = 'mz_scout_filter_panel_state'; function savePanelState(isOpen) { localStorage.setItem(PANEL_STATE_KEY, isOpen ? 'open' : 'closed'); } function getSavedPanelState() { return localStorage.getItem(PANEL_STATE_KEY) === 'open'; } // AGRESSIVE CLEANUP: Eski tüm versiyonları temizle Object.keys(localStorage).forEach(key => { if (key.startsWith('mz_scout_report_cache_')) { localStorage.removeItem(key); } }); // Sayfa dilini tespit et const pageLang = document.documentElement.lang || $('meta[name="language"]').attr('content') || 'en'; const PERSISTENT_CACHE_KEY = 'mz_scout_report_cache_v10_' + pageLang; let persistentCache = {}; let currentRosterPids = new Set(); let isSyncing = false; // --- DİL DESTEĞİ (i18n) --- const i18n_scout = { tr: { filterTitle: "Scout Filtreleri", filterHP: "Y.P (Yüksek Potansiyel):", filterLP: "D.P (Düşük Potansiyel):", filterTS: "A.H (Antrenman Hızı):", applyBtn: "Filtrele", filterCount: (visible, total) => `${visible} / ${total} Oyuncu Listeleniyor`, loadingStatus: (newCount) => newCount > 0 ? `Önbellek güncelleniyor: ${newCount} yeni oyuncu taranıyor...` : `Önbellek hazır.`, unscouted: "Taranmamış", selectAll: "Hepsini Seç", deselectAll: "Hepsini Kaldır", btnRefreshCache: "Önbelleği Yenile (Tam Tarama)", alertRefreshConfirm: "Tüm oyuncu verileri silinecek ve yeniden taranacak. Emin misiniz?", }, en: { filterTitle: "Scout Filters", filterHP: "HP (High Potential):", filterLP: "LP (Low Potential):", filterTS: "TS (Train Speed):", applyBtn: "Filter", filterCount: (visible, total) => `${visible} / ${total} Players Displayed`, loadingStatus: (newCount) => newCount > 0 ? `Cache updating: Scanning ${newCount} new players...` : `Cache ready.`, unscouted: "Unscouted", selectAll: "Select All", deselectAll: "Deselect All", btnRefreshCache: "Refresh Cache (Full Scan)", alertRefreshConfirm: "The cache for all player data will be deleted and rescanned. Are you sure?", } }; const lang = ($('meta[name="language"]').attr('content') || 'en') === 'tr' ? 'tr' : 'en'; const getText = (key, ...args) => { const t = i18n_scout[lang][key] || i18n_scout['en'][key]; return typeof t === 'function' ? t(...args) : t; }; // --- CSS STİLLERİ --- GM_addStyle(` .gm_scout_h { font-weight: bold; } .gm_s1 { color: red !important; } .gm_s2 { color: darkgoldenrod !important; } .gm_s3 { color: blue !important; } .gm_s4 { color: fuchsia !important; } .skill_name_colored { transition: color 0.3s; } #mz-scout-filter-panel-wrapper { margin-top: 10px; margin-bottom: -1px; border: 1px solid #ddd; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } #mz-scout-filter-toggle-btn { background: #007bff; color: white; padding: 5px 10px; font-weight: bold; font-size: 14px; cursor: pointer; border-radius: 4px 4px 0 0; text-align: center; user-select: none; transition: background 0.2s; } #mz-scout-filter-toggle-btn:hover { background: #0056b3; } #mz-scout-filter-panel { padding: 10px; background: #f8f9fa; border-top: 1px solid #ddd; border-radius: 0 0 5px 5px; display: flex; flex-direction: column; gap: 10px; } .mz-filter-group { display: flex; align-items: center; gap: 8px; border: 1px solid #ccc; padding: 5px 10px; border-radius: 4px; } .mz-filter-group label { font-size: 13px; cursor: pointer; user-select: none; } .mz-filter-group input[type="checkbox"] { margin-right: 2px; accent-color: #00529B; } #mz-filter-count { font-weight: bold; color: #00529B; font-size: 13px; } #mz-scout-filter-panel { margin-top: 10px; padding: 10px; background: #f8f9fa; border: 1px solid #ddd; border-radius: 5px; display: flex; flex-wrap: wrap; gap: 15px; align-items: center; justify-content: space-between; } #mz-filter-actions { display: flex; gap: 10px; margin-top: 10px; } .mz-action-link { font-size: 12px; color: #007bff; text-decoration: none; cursor: pointer; font-weight: bold; } .mz-action-link:hover { text-decoration: underline; } #mz-apply-filter-btn { display: none !important; } .mz-refresh-btn { padding: 4px 10px; background: #FF9800; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 11px; transition: background 0.2s; } .mz-refresh-btn:hover { background: #E68900; } `); // --- YARDIMCI FONKSİYONLAR --- // Global debounce utility used. async function saveCache() { setTimeout(async () => { try { await GM_setValue(PERSISTENT_CACHE_KEY, JSON.stringify(persistentCache)); } catch (e) { console.error("[MZ Scout] saveCache failed:", e); } }, 0); } function getPid(container) { let pid = container.find('.player_id_span').first().text().trim() || null; if (!pid) { pid = container.closest('div, tr, tbody, table').parent().find('.player_id_span').first().text().trim() || null; } if (!pid) { const href = container.find('a[href*="pid="], a.player_link').attr('href') || container.closest('div, tr, tbody, table').parent().find('a[href*="pid="], a.player_link').attr('href'); if (href) { const match = href.match(/pid=(\d+)/); if (match) pid = match[1]; } } if (!pid && window.location.href.includes('p=players') && window.location.href.includes('pid=')) { const urlParams = new URLSearchParams(window.location.search); pid = urlParams.get('pid'); } return pid; } async function syncRosterAndCache() { if (isSyncing) return []; isSyncing = true; currentRosterPids.clear(); // Anchor strictly onto table.player_skills const $skillTables = $('table.player_skills'); $skillTables.each(function () { // Parent wrapper to colorize let $container = $(this).closest('.playerContainer'); if ($container.length === 0) $container = $(this).closest('#playerInfoWindow'); if ($container.length === 0) $container = $(this).parent(); const pid = getPid($container); if (pid) currentRosterPids.add(pid); }); try { persistentCache = JSON.parse(await GM_getValue(PERSISTENT_CACHE_KEY, '{}')); } catch (e) { persistentCache = {}; } const newPids = []; // Do not delete cache items if they are not in the current view to avoid wiping the cache on single-player pages. // for (const pid in persistentCache) { if (!currentRosterPids.has(pid)) delete persistentCache[pid]; } currentRosterPids.forEach(pid => { if (pid !== '_last_scan_date' && !persistentCache.hasOwnProperty(pid)) { newPids.push(pid); } }); await saveCache(); isSyncing = false; return newPids; } function applyColors(container, report) { // Hata önleyici kontrol: Rapor objesi veya içindeki alanlar eksikse işlemi durdur if (!report || !report.hp || !report.lp || !report.ts) return; let skillElements = container.find("table.player_skills td > span.clippable, .player-skills span.responsive-hide.responsive-container, td > span.skill_name span:first-child"); if (skillElements.length === 0 && container.is('table.player_skills')) { skillElements = container.find("td > span.clippable"); } if (skillElements.length === 0) skillElements = container.find('td').filter(function (index) { return index >= 5 && index <= 15; }).find('span:first'); container.find('.gm_scout_h, .gm_s1, .gm_s2, .gm_s3, .gm_s4').removeClass('gm_scout_h gm_s1 gm_s2 gm_s3 gm_s4 skill_name_colored'); skillElements.each(function () { const el = $(this); const skillName = el.text().trim(); const parent = el.parent(); // HP ve LP kontrollerine güvenlik eklemesi if (report.hp.skills && report.hp.skills.includes(skillName)) { parent.addClass(`gm_scout_h gm_s${report.hp.stars}`); el.addClass(`gm_scout_h gm_s${report.hp.stars} skill_name_colored`); } else if (report.lp.skills && report.lp.skills.includes(skillName)) { parent.addClass(`gm_s${report.lp.stars}`); el.addClass(`gm_s${report.lp.stars} skill_name_colored`); } }); container.attr({ 'data-hp-stars': report.hp.stars || 0, 'data-lp-stars': report.lp.stars || 0, 'data-ts-stars': report.ts.stars || 0, 'data-unscouted': report.unscouted }).show(); // --- YP/DP Başlık Metni Eklemesi --- let headerEl = container.find('h2.subheader, h2.clearfix').first(); if (headerEl.length > 0) { container.find('.mz-scout-text-info, .mz-scout-text-info-row').remove(); // Varsa temizle let textArr = []; const hpLabel = 'Y.P'; const lpLabel = 'D.P'; if (report.hp && report.hp.skills && report.hp.skills.length > 0) { textArr.push(`<span class="gm_s${report.hp.stars}">${hpLabel} ${report.hp.stars}: ${report.hp.skills.join('-')}</span>`); } if (report.lp && report.lp.skills && report.lp.skills.length > 0) { textArr.push(`<span class="gm_s${report.lp.stars}">${lpLabel} ${report.lp.stars}: ${report.lp.skills.join('-')}</span>`); } if (textArr.length > 0) { let infoRow = $(`<div class="mz-scout-text-info-row" style="background: #fcfcfc; border: 1px solid #ececec; border-left: 3px solid #ccc; padding: 3px 10px; margin-top: 5px; margin-bottom: 8px; margin-left: 0px; border-radius: 2px; display: inline-block; box-shadow: 0 1px 2px rgba(0,0,0,0.02); font-size:12px; font-weight:bold; letter-spacing:0.5px;">${textArr.join(' <span style="color:#ccc; font-weight:normal; margin: 0 4px;">|</span> ')}</div>`); headerEl.after(infoRow); } } } function fetchSingleReport(pid, attempt = 1) { return new Promise((resolve) => { const url = `/ajax.php?p=players&sub=scout_report&pid=${pid}&sport=soccer&lang=tr`; const timeoutDuration = 8000; let isHandled = false; const timeout = setTimeout(() => { if (!isHandled) { isHandled = true; if (attempt < 2) { resolve(fetchSingleReport(pid, attempt + 1)); } else { resolve({ pid: pid, hp: { stars: 0, skills: [] }, lp: { stars: 0, skills: [] }, ts: { stars: 0 }, unscouted: true, failed: true }); } } }, timeoutDuration); GM_xmlhttpRequest({ method: "GET", url: url, timeout: timeoutDuration, onload: function (response) { if (isHandled) return; isHandled = true; clearTimeout(timeout); try { const parser = new DOMParser(); const doc = parser.parseFromString(response.responseText, "text/html"); const divs = doc.querySelectorAll("dd div.flex-grow-1"); const starSpans = doc.querySelectorAll("dd span.stars"); const countStars = (span) => span ? span.querySelectorAll(".lit").length : 0; const extractSkills = (el) => Array.from(el.querySelectorAll(".blurred span")).map(s => s.textContent.trim()).filter(t => t !== "Trzxyvopaxis" && t.length > 1); let hpSkills = [], lpSkills = []; let hpStars = 0, lpStars = 0, tsStars = 0; let unscouted = starSpans.length === 0; if (divs.length >= 2) { hpStars = countStars(starSpans[0]); hpSkills = extractSkills(divs[0]); lpStars = countStars(starSpans[1]); lpSkills = extractSkills(divs[1]); if (starSpans.length > 2) tsStars = countStars(starSpans[2]); } resolve({ pid: pid, hp: { stars: hpStars, skills: hpSkills }, lp: { stars: lpStars, skills: lpSkills }, ts: { stars: tsStars }, unscouted: unscouted }); } catch (e) { resolve({ pid: pid, unscouted: true, failed: true, hp: { stars: 0, skills: [] }, lp: { stars: 0, skills: [] }, ts: { stars: 0 } }); } }, onerror: function (err) { if (isHandled) return; isHandled = true; clearTimeout(timeout); resolve({ pid: pid, unscouted: true, failed: true, hp: { stars: 0, skills: [] }, lp: { stars: 0, skills: [] }, ts: { stars: 0 } }); } }); }); } async function fetchNewReports(newPids) { const statusEl = $('#mz-filter-count'); if (newPids.length === 0) { statusEl.text(getText('loadingStatus', 0)); return; } window._mzRosterScoutActive = true; let processedCount = 0; const total = newPids.length; const concurrency = 4; const queue = [...newPids]; const activeRequests = new Set(); return new Promise((resolve) => { const processNext = async () => { if (queue.length === 0 && activeRequests.size === 0) { await saveCache(); statusEl.text(getText('loadingStatus', 0)); window._mzRosterScoutActive = false; resolve(); return; } while (activeRequests.size < concurrency && queue.length > 0) { const pid = queue.shift(); activeRequests.add(pid); fetchSingleReport(pid).then(async (report) => { persistentCache[report.pid] = report; processedCount++; statusEl.text(getText('loadingStatus', total - processedCount)); if (!document.hidden) { let container = $(`.playerContainer:has(.player_id_span:contains('${report.pid}'))`); if (container.length === 0) { $('table.player_skills').each(function () { const temp = $(this).closest('#playerInfoWindow, .playerContainer, table').parent(); if (getPid(temp) === report.pid.toString()) { container = temp; return false; } }); } if (container.length) applyColors(container, report); } if (processedCount % 30 === 0) await saveCache(); activeRequests.delete(pid); setTimeout(processNext, 150); }).catch(async (err) => { processedCount++; statusEl.text(getText('loadingStatus', total - processedCount)); activeRequests.delete(pid); setTimeout(processNext, 150); }); } }; processNext(); }); } function addScoutFilterUI() { // Do not show filter UI in popup or compact views const pParam = new URLSearchParams(window.location.search).get('p'); if (pParam === 'training_camp' || pParam === 'match' || pParam === 'tactics' || pParam === 'transfer' || pParam === 'shortlist') return; // Try to find the first container with a subheader (standard view) let target = $('.playerContainer h2.subheader').first().closest('.playerContainer'); // If not found, fall back to the first player container (search results view) if (target.length === 0) { target = $('.playerContainer').first(); } if (target.length === 0) return; // If panel already exists, don't recreate it (prevents flickering) if ($('#mz-scout-filter-panel-wrapper').length > 0) { return; } // ... (Filtre UI kodları orijinalindeki gibi kalacak, yer kaplamasın diye kısalttım, işlev aynı) const filterHTML = `<div id="mz-scout-filter-panel" style="padding:10px;background:#f8f9fa;border:1px solid #ddd;border-radius:0 0 5px 5px;display:flex;flex-direction:column;gap:10px;"><div style="display:flex;flex-wrap:wrap;gap:15px;align-items:center;justify-content:space-between;"><div class="mz-filter-group" data-filter-type="hp"><span style="font-weight:bold;color:#00529B;">${getText('filterHP')}</span><label><input type="checkbox" class="mz-scout-filter" data-filter-type="hp" data-star="4"> 4★</label><label><input type="checkbox" class="mz-scout-filter" data-filter-type="hp" data-star="3"> 3★</label><label><input type="checkbox" class="mz-scout-filter" data-filter-type="hp" data-star="2"> 2★</label></div><div class="mz-filter-group" data-filter-type="lp"><span style="font-weight:bold;color:#00529B;">${getText('filterLP')}</span><label><input type="checkbox" class="mz-scout-filter" data-filter-type="lp" data-star="2"> 2★</label><label><input type="checkbox" class="mz-scout-filter" data-filter-type="lp" data-star="1"> 1★</label></div><div class="mz-filter-group" data-filter-type="ts"><span style="font-weight:bold;color:#00529B;">${getText('filterTS')}</span><label><input type="checkbox" class="mz-scout-filter" data-filter-type="ts" data-star="4"> 4★</label><label><input type="checkbox" class="mz-scout-filter" data-filter-type="ts" data-star="3"> 3★</label><label><input type="checkbox" class="mz-scout-filter" data-filter-type="ts" data-star="2"> 2★</label><label><input type="checkbox" class="mz-scout-filter" data-filter-type="ts" data-star="1"> 1★</label></div><select id="mz-sort-positions-select" autocomplete="off" style="background:#27ae60;color:white;border:none;padding:5px 10px;border-radius:4px;cursor:pointer;font-weight:bold;box-shadow:0 2px 4px rgba(0,0,0,0.2);margin-left:auto;outline:none;"><option value="ALL">Mevkiye Göre Sırala (Genel)</option><option value="GK">Sırala: Kaleci (GK)</option><option value="DF">Sırala: Stoper (DF)</option><option value="ANC">Sırala: Ön Libero (ANC)</option><option value="MF">Sırala: Orta Saha (MF)</option><option value="WG">Sırala: Kanat (WG)</option><option value="FW">Sırala: Santrafor (FW)</option></select></div><div style="display:flex;justify-content:space-between;align-items:center;border-top:1px solid #dee2e6;padding-top:10px;"><span id="mz-filter-count" style="font-weight:bold;">${getText('loadingStatus', 0)}</span><div id="mz-filter-actions"><button id="mz-refresh-cache-btn" class="mz-refresh-btn">${getText('btnRefreshCache')}</button><a href="#" id="mz-select-all-filters" class="mz-action-link">${getText('selectAll')}</a><a href="#" id="mz-deselect-all-filters" class="mz-action-link">${getText('deselectAll')}</a></div></div></div>`; const wrapperHTML = `<div id="mz-scout-filter-panel-wrapper" style="margin-top:10px;margin-bottom:-1px;border:1px solid #ddd;border-bottom:none;border-radius:5px 5px 0 0;"><div id="mz-scout-filter-toggle-btn" style="background:#007bff;color:white;padding:5px 10px;font-weight:bold;font-size:14px;cursor:pointer;border-radius:4px 4px 0 0;text-align:center;user-select:none;">🔍 ${getText('filterTitle')}</div>${filterHTML}</div>`; target.before(wrapperHTML); $('#mz-scout-filter-panel').hide(); $('#mz-sort-positions-select').val('ALL'); $('#mz-scout-filter-toggle-btn').on('click', (e) => { e.preventDefault(); const panel = $('#mz-scout-filter-panel'); panel.slideToggle(200, function () { savePanelState(panel.is(':visible')); }); $('#mz-scout-filter-toggle-btn').css('background', panel.is(':visible') ? '#dc3545' : '#007bff'); }); const debouncedFilter = debounce(applyScoutFilters, 150); $('.mz-scout-filter').on('change', debouncedFilter); $('#mz-refresh-cache-btn').on('click', async (e) => { e.preventDefault(); if (confirm(getText('alertRefreshConfirm'))) { await GM_deleteValue(PERSISTENT_CACHE_KEY); savePanelState(true); alert("Önbellek temizlendi."); location.reload(); } }); $('#mz-select-all-filters').on('click', (e) => { e.preventDefault(); $('.mz-scout-filter').prop('checked', true).trigger('change'); }); $('#mz-deselect-all-filters').on('click', (e) => { e.preventDefault(); $('.mz-scout-filter').prop('checked', false).trigger('change'); }); $('#mz-sort-positions-select').on('change', function(e) { e.preventDefault(); const players = $('#mz-scout-filter-panel-wrapper').nextAll('.playerContainer').get(); if (players.length === 0) return; const posOrder = { "GK": 1, "DF": 2, "ANC": 3, "MF": 4, "WG": 5, "FW": 6 }; const selectedPos = $(this).val(); const colorMap = { "GK": "#e67e22", "DF": "#27ae60", "ANC": "#16a085", "MF": "#2980b9", "WG": "#8e44ad", "FW": "#e74c3c" }; const langMeta = document.querySelector('meta[name="language"]'); const isTr = langMeta && langMeta.getAttribute('content') === 'tr'; const nameMap = isTr ? { "GK": "Kaleci", "DF": "Stoper", "ANC": "Ön Libero", "MF": "Orta Saha", "WG": "Kanat", "FW": "Santrafor" } : { "GK": "GK", "DF": "CB", "ANC": "ANC", "MF": "MF", "WG": "Winger", "FW": "Striker" }; players.sort((a, b) => { if (selectedPos === "ALL") { const orderA = parseInt($(a).attr('data-mz-initial-order') || 999); const orderB = parseInt($(b).attr('data-mz-initial-order') || 999); return orderA - orderB; } else { const badgeA = $(a).find('.mz-ai-position-badge'); const badgeB = $(b).find('.mz-ai-position-badge'); const posLower = selectedPos.toLowerCase(); const scoreA = badgeA.length ? parseFloat(badgeA.attr('data-mz-score-' + posLower) || 0) : 0; const scoreB = badgeB.length ? parseFloat(badgeB.attr('data-mz-score-' + posLower) || 0) : 0; return scoreB - scoreA; } }); let currentEl = $('#mz-scout-filter-panel-wrapper'); $.each(players, function(idx, itm) { currentEl.after(itm); currentEl = $(itm); const badge = $(itm).find('.mz-ai-position-badge'); if (badge.length) { if (selectedPos === "ALL") { if (badge.attr('data-mz-orig-text')) { badge.text(badge.attr('data-mz-orig-text')); badge.css('background-color', badge.attr('data-mz-orig-bg')); } } else { const score = badge.attr('data-mz-score-' + selectedPos.toLowerCase()) || "0.00"; badge.text(nameMap[selectedPos] + ": " + score); badge.css('background-color', colorMap[selectedPos]); } } }); }); if (getSavedPanelState()) { $('#mz-scout-filter-panel').show(); $('#mz-scout-filter-toggle-btn').css('background', '#dc3545'); } else { savePanelState(false); } } function applyScoutFilters() { // Profil detay sayfasındaysak (liste değilse) filtreleme yapma if (window.location.search.includes('sub=players') || window.location.search.includes('pid=')) { $('.playerContainer, #playerInfoWindow').show(); return; } const filters = { hp: $('.mz-scout-filter[data-filter-type="hp"]:checked').map(function () { return parseInt($(this).data('star')); }).get(), lp: $('.mz-scout-filter[data-filter-type="lp"]:checked').map(function () { return parseInt($(this).data('star')); }).get(), ts: $('.mz-scout-filter[data-filter-type="ts"]:checked').map(function () { return parseInt($(this).data('star')); }).get() }; // "Tümünü Seç" ve "Hiçbiri Seçili Değil" durumu kontrolü const isAllHpSelected = filters.hp.length === 3; // 4, 3, 2 const isAllLpSelected = filters.lp.length === 2; // 2, 1 const isAllTsSelected = filters.ts.length === 4; // 4, 3, 2, 1 const isAllSelected = isAllHpSelected && isAllLpSelected && isAllTsSelected; const isNoneHpSelected = filters.hp.length === 0; const isNoneLpSelected = filters.lp.length === 0; const isNoneTsSelected = filters.ts.length === 0; const isNoneSelected = isNoneHpSelected && isNoneLpSelected && isNoneTsSelected; const showEverything = isNoneSelected; let visibleCount = 0, totalCount = 0; const processedPids = new Set(); $('.playerContainer, #playerInfoWindow').each(function () { const container = $(this); // Aynı oyuncuyu (hem infoWindow hem listede) iki kere saymamak için PID kontrolü const pid = getPid(container); if (!pid || processedPids.has(pid)) return; processedPids.add(pid); const report = persistentCache[pid]; totalCount++; // Eğer oyuncu verisi bozuksa veya yoksa filtreleme sırasında hata vermemesi için: if (!report || report.unscouted || !report.hp || !report.lp || !report.ts) { if (!report || (showEverything && (report.unscouted || true))) { // Rapor yoksa VEYA ("Tümünü Seç" iken) GÖSTER container.show(); visibleCount++; } else { // Filtreleme aktif ve rapor unscouted ise GİZLE container.hide(); } return; } const hpMatch = filters.hp.length === 0 || filters.hp.includes(report.hp.stars); const lpMatch = filters.lp.length === 0 || filters.lp.includes(report.lp.stars); const tsMatch = filters.ts.length === 0 || filters.ts.includes(report.ts.stars); if (hpMatch && lpMatch && tsMatch) { container.show(); visibleCount++; } else { container.hide(); } }); $('#mz-filter-count').text(getText('filterCount', visibleCount, totalCount)); } function processPlayersInitial() { let orderIdx = 0; $('table.player_skills').each(function () { let container = $(this).closest('.playerContainer'); if (container.length === 0) container = $(this).closest('#playerInfoWindow'); if (container.length === 0) container = $(this).closest('table').parent(); if (container.length === 0) container = $(this).parent(); if (container.length && !container.attr('data-mz-initial-order')) { container.attr('data-mz-initial-order', orderIdx++); } const pid = getPid(container); if (pid && persistentCache[pid]) applyColors(container, persistentCache[pid]); }); addScoutFilterUI(); applyScoutFilters(); } let isInitializing = false; async function init() { if (isInitializing) return; isInitializing = true; window._mzRosterScoutActive = true; try { const newPids = await syncRosterAndCache(); processPlayersInitial(); if (newPids.length > 0) { await fetchNewReports(newPids); applyScoutFilters(); } } finally { isInitializing = false; window._mzRosterScoutActive = false; } } setTimeout(init, 500); // Re-initialize when DOM changes (e.g., after search results load or popup opens) // PERFORMANS OPTİMİZASYONU: debounce 1000ms — init() scout API çağrıları başlatır, // çok sık tetiklenirse scroll sırasında gereksiz network istekleri oluşur. // DOM değişikliklerini izlemek yerine 1.5 saniyede bir sessizce kontrol et (JANK önleyici) setInterval(() => { if (document.querySelector('table.player_skills')) { init(); } }, 1500); } /**************************************************************************************** * * * BÖLÜM 10: YETENEK RENKLENDİRİCİ (GÜNCELLENMİŞ - ÇİFT TOP HATASI GİDERİLDİ) * * * Bölüm 22'deki "Processed" ve "Select-Based" mantığı entegre edildi. * * * ****************************************************************************************/ function initializeMaxedBallColoringScript() { 'use strict'; const $ = jQuery; function debounce(func, wait) { let timeout; return function (...args) { const context = this; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, args), wait); }; } // KORUMA: Transfer, Shortlist veya Maç sayfalarında çalışmaz. const pParam = new URLSearchParams(window.location.search).get('p'); if (pParam === 'transfer' || pParam === 'shortlist' || pParam === 'match') return; const IMG_RED = "data:image/gif;base64,R0lGODlhDAAKAJEDAP////8AAMyZmf///yH5BAEAAAMALAAAAAAMAAoAAAIk3BQZYp0CAAptxvjMgojTEVwKpl0dCQrQJX3T+jpLNDXGlDUFADs="; const IMG_BLUE = "data:image/gif;base64,R0lGODlhDAAKAJEDAP///8zM/wAA/////yH5BAEAAAMALAAAAAAMAAoAAAIk3CIpYZ0BABJtxvjMgojTIVwKpl0dCQbQJX3T+jpLNDXGlDUFADs="; function generateBallHtml(count, imgSource, extraIconsHtml) { if (!count || count < 0) return ''; let html = '<div class="mz-custom-skill-container" style="display:inline-flex; align-items:center; vertical-align:middle; white-space:nowrap; height: 12px;">'; for (let i = 0; i < count; i++) { html += `<img src="${imgSource}" style="vertical-align:middle; margin-right:1px;">`; } if (extraIconsHtml) { html += `<span class="mz-training-icon-wrapper" style="margin-left:0px; display:inline-flex; align-items:center;">${extraIconsHtml}</span>`; } html += '</div>'; return html; } function colorizeBalls() { // Bölüm 22 Mantığı: Alttaki antrenman seçiminden hangi yeteneğin çalışıldığını bul const todayTrainingSkill = $('select[name="training_field"] option:selected, select.training-attr-selector option:selected').first().text().trim(); $('img.skill').each(function () { const img = $(this); const parentRow = img.closest('tr'); // KRİTİK DÜZELTME: Eğer zaten işlenmişse (processed) ATLA // Popup içerisinde gizli ('display: none') olarak DOM'a eklendiğinde :hidden engellemesini kaldırdık. if (img.hasClass('mz-skill-processed')) return; if (img.next().hasClass('mz-custom-skill-container')) return; const skillLabelTd = parentRow.find('td:first'); const skillName = skillLabelTd.text().trim(); const skillLabelLower = skillName.toLowerCase(); // Tecrübe ve Form satırlarını boyama (Hizalamayı düzeltmek için hafif sola çek) if (skillLabelLower.includes('tecrübe') || skillLabelLower.includes('experience') || skillLabelLower.includes('form')) { img.parent().css('margin-left', '-3px'); return; } // Yetenek değerini çek (Bölüm 22 gibi parantez içini de kontrol eder) let skillValue = 0; const altText = img.attr('alt') || ''; if (altText.includes(':')) { skillValue = parseInt(altText.split(':')[1].trim(), 10); } else { const textMatch = parentRow.text().match(/\((\d+)\)/); if (textMatch) skillValue = parseInt(textMatch[1], 10); } if (!skillValue || isNaN(skillValue)) return; // MAXED Kararı (Sadece sayı kısmındaki kırmızı renk/maxed class kontrol edilir) const skillValueTd = parentRow.find('td.skillval'); let isMaxed = skillValueTd.find('.maxed').length > 0; if (!isMaxed) { const color = skillValueTd.find('span').css('color'); // FIX: Ignore red if it comes from our scout coloring (gm_s1) isMaxed = ((color === 'rgb(255, 0, 0)' || color === 'red' || skillValueTd.find('font[color="red"]').length > 0) && !skillValueTd.hasClass('gm_s1') && !parentRow.hasClass('gm_s1')); } const targetImg = isMaxed ? IMG_RED : IMG_BLUE; // Yeşil ikon mantığı (Bölüm 22: İsme göre veya blevel ikonuna göre) let extraIconsHtml = ''; const hasBlevelIcon = parentRow.find('img[src*="blevel"]').length > 0; if (skillName === todayTrainingSkill || hasBlevelIcon) { extraIconsHtml = '<img src="data:image/gif;base64,R0lGODlhBgAKAJEDAJnMZpmZmQAAAP///yH5BAEAAAMALAAAAAAGAAoAAAIRXCRhApAMgoPtVXXS2Lz73xUAOw==" style="vertical-align:middle;">'; // Orijinal yeşil ikonu gizle ki çift görünmesin parentRow.find('img[src*="blevel"]').hide().addClass('mz-skill-processed'); } // UYGULAMA: Orijinal siyah topu gizle ve işaretle img.hide().addClass('mz-skill-processed'); const newHtml = generateBallHtml(skillValue, targetImg, extraIconsHtml); img.after(newHtml); }); } // İlk çalıştırma setTimeout(colorizeBalls, 500); // Eski tarz setInterval ile kontrol (Scroll JANK'i önler) setInterval(colorizeBalls, 1500); } /**************************************************************************************** * * * BÖLÜM 10.1: SADECE TRANSFER MARKET RENKLENDİRME (KARARLI SÜRÜM) - DÜZELTİLDİ v2 * * * ****************************************************************************************/ function initializeTransferMarketColoring() { 'use strict'; const $ = jQuery; const IMG_RED = "data:image/gif;base64,R0lGODlhDAAKAJEDAP////8AAMyZmf///yH5BAEAAAMALAAAAAAMAAoAAAIk3BQZYp0CAAptxvjMgojTEVwKpl0dCQrQJX3T+jpLNDXGlDUFADs="; const IMG_BLUE = "data:image/gif;base64,R0lGODlhDAAKAJEDAP///8zM/wAA/////yH5BAEAAAMALAAAAAAMAAoAAAIk3CIpYZ0BABJtxvjMgojTIVwKpl0dCQbQJX3T+jpLNDXGlDUFADs="; function process() { // Sadece orijinal topları seç $('img[src*="skillball"], img.skill').not('.mz-done, .mz-processed-ball img').each(function () { const img = $(this); const skillContainer = img.parent(); // Aynı hücreyi (yeteneği) zaten işlediysek işlemi atla if (skillContainer.attr('data-mz-row-processed') === 'true') return; skillContainer.attr('data-mz-row-processed', 'true'); // Güvenlik: Sadece aynı hücre/td içindeki top sayısını say. const originalBalls = skillContainer.find('img[src*="skillball"], img.skill').not('.mz-done, .mz-processed-ball img'); if (originalBalls.length > 25) return; const row = img.closest('tr, li, .skill-row, dl'); let skillLabelLower = ""; let skillLabelTd = null; if (row.find('td, th').length >= 2) { skillLabelTd = img.closest('td').prev('td, th'); if (skillLabelTd.length === 0) skillLabelTd = row.find('td:first'); skillLabelLower = skillLabelTd.text().toLowerCase(); } else if (img.parent().prev().length > 0) { skillLabelLower = img.parent().prev().text().toLowerCase(); } else { skillLabelLower = row.text().toLowerCase(); } if (skillLabelLower.includes('tecrübe') || skillLabelLower.includes('experience') || skillLabelLower.includes('form')) { skillContainer.css('margin-left', '-3px'); return; // Tecrübe ve Form boyanmaz } // Değer tespiti: 1. Alt text, 2. Src rakamı, 3. Konteynırdaki toplam top sayısı let val = parseInt((img.attr('alt') || "").split(':')[1]) || parseInt((img.attr('src').match(/(\d+)\.gif/) || [])[1]); if (!val) { val = originalBalls.length; } if (!val || val <= 0) return; // Tıkanıklık tespiti let isMaxed = row.find('.maxed').length > 0 || skillContainer.find('.maxed').length > 0 || skillContainer.attr('data-mz-maxed') === 'true'; if (!isMaxed && skillLabelTd) { const color = skillLabelTd.css('color'); // FIX: Eğer kırmızı renk gm_s1 sınıfından geliyorsa (DP=1), bu tıkalı olduğu anlamına gelmez. if ((color === 'rgb(255, 0, 0)' || color === 'red' || skillLabelTd.find('font[color="red"]').length > 0) && !skillLabelTd.hasClass('gm_s1')) { isMaxed = true; } } let html = '<div class="mz-processed-ball" style="display:inline-flex; align-items:center; height:12px; vertical-align:middle;">'; const ballImg = isMaxed ? IMG_RED : IMG_BLUE; for (let i = 0; i < val; i++) html += `<img src="${ballImg}" style="margin-right:1px; width:12px; height:10px;">`; html += '</div>'; // Tüm orijinal topları gizle originalBalls.addClass('mz-done').hide(); // Yeni topları ekle skillContainer.append(html); }); } // İlk tetikleme setTimeout(process, 1000); setTimeout(process, 3000); // Yedek tetikleme // Global'e kaydet - TSF filtrelemesi sonrası çağırabilsin window._mzColorBalls = process; // Yedek olarak 1.5 saniyede bir sessiz tarama yap setInterval(process, 1500); } /**************************************************************************************** * * * BÖLÜM 10.2: SADECE TAKİP LİSTESİ RENKLENDİRME (GÜNCEL - ETİKET DESTEKLİ) * * * ****************************************************************************************/ function initializeShortlistColoring() { 'use strict'; const $ = jQuery; const IMG_RED = "data:image/gif;base64,R0lGODlhDAAKAJEDAP////8AAMyZmf///yH5BAEAAAMALAAAAAAMAAoAAAIk3BQZYp0CAAptxvjMgojTEVwKpl0dCQrQJX3T+jpLNDXGlDUFADs="; const IMG_BLUE = "data:image/gif;base64,R0lGODlhDAAKAJEDAP///8zM/wAA/////yH5BAEAAAMALAAAAAAMAAoAAAIk3CIpYZ0BABJtxvjMgojTIVwKpl0dCQbQJX3T+jpLNDXGlDUFADs="; // Session Cache for Shortlist (To avoid repetitive network requests) const sl_cache = {}; // Antrenman grafiğinden tıkalı özellikleri tespit et (aynı fonksiyon text coloring'den) // Antrenman grafiğinden tıkalı özellikleri tespit et (JSON API yöntemi) async function getMaxedSkillsFromTrainingGraph(pid) { if (sl_cache[pid]) return sl_cache[pid]; console.log(`Tıkalı özellikleri JSON ile kontrol et (PID: ${pid})...`); try { // HTML parse etmek yerine doğrudan JSON verisini çekiyoruz const url = `/ajax.php?p=trainingGraph&sub=getJsonTrainingHistory&sport=soccer&player_id=${pid}`; const response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: resolve, onerror: reject }); }); if (!response.responseText) return []; let jsonStr = response.responseText; // Sunucu cevabı JavaScript kodu içeriyor: "var series = [ ... ];" // Basit substring/lastIndexOf yetersiz kalabiliyor (birden fazla dizi olabilir). // "var series =" sonrası ilk '[' ile başlayıp, parantez sayarak doğru kapatmayı bulacağız. const varIndex = jsonStr.indexOf('var series'); if (varIndex !== -1) { const startIndex = jsonStr.indexOf('[', varIndex); if (startIndex !== -1) { let openCount = 0; let endIndex = -1; // Parantezleri sayarak ilerle for (let i = startIndex; i < jsonStr.length; i++) { if (jsonStr[i] === '[') openCount++; else if (jsonStr[i] === ']') openCount--; if (openCount === 0) { endIndex = i; break; } } if (endIndex !== -1) { jsonStr = jsonStr.substring(startIndex, endIndex + 1); } } } let series; try { const responseText = response.responseText.trim(); if (responseText.startsWith('var series')) { const func = new Function(responseText + '; return series;'); series = func(); } else { series = JSON.parse(responseText); } } catch (e) { console.error('Antrenman verisi parse edilemedi:', e); return []; } // const series satırı kaldırıldı, yukarıda let series olarak tanımlandı. const maxedIndices = []; if (!series || !Array.isArray(series)) return []; series.forEach(s => { // 1. Kırmızı Çizgi Kontrolü (rgba(255,0,0,0.7)) // Highcharts'ta maxed çizgisi genellikle bu renktedir if (s.type === 'line' && (s.color === 'rgba(255,0,0,0.7)' || s.color === 'red' || s.color === 'rgb(255,0,0)')) { if (s.data && s.data.length > 0) { // y değeri, skill sırasını (1-11) belirtir. Index'e çevirmek için -1 yapıyoruz. const y = s.data[0].y; const idx = y - 1; if (idx >= 0 && !maxedIndices.includes(idx)) { maxedIndices.push(idx); } } } // 2. Nokta Kontrolü ("Maxed" etiketi) // Bazen çizgi yerine nokta marker'ı olabilir else if (s.data) { s.data.forEach(d => { if (d.name === 'Maxed') { const idx = d.y - 1; if (idx >= 0 && !maxedIndices.includes(idx)) { maxedIndices.push(idx); } } }); } }); console.log(`🎯 JSON Sonuç (PID ${pid}): Tıkalı indeksler:`, maxedIndices); sl_cache[pid] = maxedIndices; return maxedIndices; } catch (error) { console.error('JSON Antrenman verisi hatası:', error); return []; } } async function process() { $('.playerContainer').each(async function () { const container = $(this); const pid = container.find('.player_id_span').text().trim(); if (!pid) { // PID yoksa eski yönteme geri dön container.find('img.skill').each(function () { const img = $(this); if (img.hasClass('sl-done')) return; const row = img.closest('tr'); let val = parseInt((img.attr('alt') || "").split(':')[1]); if (!val) { const m = img.attr('src').match(/\/(\d+)\.gif/); if (m) val = parseInt(m[1]); } if (!val) return; // --- TIKALI TESPİT MANTIĞI --- let isMaxed = false; // 1. Durum: Diğer modül etiketi bırakmış mı? (En güvenilir) if (row.attr('data-mz-maxed') === 'true') { isMaxed = true; } // 2. Durum: Hala kırmızı font/renk var mı? if (!isMaxed) { row.find('td:first').find('*').addBack().each(function () { const $el = $(this); const c = $el.css('color'); if ((c === 'rgb(255, 0, 0)' || c === 'red') && !$el.hasClass('gm_s1') && !$el.closest('td').hasClass('gm_s1')) { isMaxed = true; return false; } }); } // 3. Durum: MZ standart tıkalı sınıfı var mı? if (!isMaxed && row.find('.maxed').length > 0) { isMaxed = true; } let html = '<div class="mz-processed-ball" style="display:inline-flex; align-items:center; height:12px; vertical-align:middle;">'; const ballImg = isMaxed ? IMG_RED : IMG_BLUE; for (let i = 0; i < val; i++) { html += `<img src="${ballImg}" style="margin-right:1px; width:12px; height:10px;">`; } html += '</div>'; img.addClass('sl-done').hide().after(html); img.parent().find('img:not(.mz-processed-ball img)').hide(); }); return; } // Antrenman grafiğinden tıkalı özellikleri al (Artık index array dönüyor: [0, 4, 7] gibi) const maxedIndices = await getMaxedSkillsFromTrainingGraph(pid); // Yetenek sırasını takip etmek için sayaç (0-10 arası) let currentSkillIndex = 0; container.find('img.skill').each(function () { const img = $(this); if (img.hasClass('sl-done')) return; const row = img.closest('tr'); const skillLabelTd = row.find('td:first'); const skillName = skillLabelTd.text().trim(); // Tecrübe ve Form satırlarını atla (Bunlar skill index'e dahil değil) const skillLower = skillName.toLowerCase(); if (skillLower.includes('tecrübe') || skillLower.includes('experience') || skillLower.includes('form')) { return; } let val = parseInt((img.attr('alt') || "").split(':')[1]); if (!val) { const m = img.attr('src').match(/\/(\d+)\.gif/); if (m) val = parseInt(m[1]); } if (!val) { // Eğer değer okunamadıysa da index artmalı mı? // Genelde img.skill varsa değer vardır. // Güvenlik için index artırıyoruz. currentSkillIndex++; return; } // --- GELİŞMİŞ TIKALI/AÇIK KONTROLÜ --- let isMaxed = false; // 1. Antrenman grafiğinden gelen tıkalı index listesini kontrol et if (maxedIndices.includes(currentSkillIndex)) { isMaxed = true; console.log(`JSON Tespiti: ${skillName} (Index ${currentSkillIndex}) -> TIKALI`); } // Bir sonraki yetenek için index'i artır currentSkillIndex++; // 2. Direkt satırda kırmızı renk kontrolü (yedek - JSON başarısız olursa veya eski veri) if (!isMaxed) { const compColor = skillLabelTd.css('color'); const hasRedFont = skillLabelTd.find('font[color="red"]').length > 0; isMaxed = (compColor === 'rgb(255, 0, 0)' || compColor === 'red' || hasRedFont); if (isMaxed) { console.log(`Kırmızı renk kontrolü: ${skillName} -> TIKALI`); } } // 3. maxed class kontrolü if (!isMaxed) { isMaxed = row.find('.maxed').length > 0; if (isMaxed) { console.log(`Maxed class kontrolü: ${skillName} -> TIKALI`); } } // 4. Tüm satırda kırmızı font ara (daha kapsamlı kontrol) if (!isMaxed) { row.find('*').addBack().each(function () { const c = $(this).css('color'); if (c === 'rgb(255, 0, 0)' || c === 'red') { isMaxed = true; console.log(`Genel renk kontrolü: ${skillName} -> TIKALI`); return false; } }); } let html = '<div class="mz-processed-ball" style="display:inline-flex; align-items:center; height:12px; vertical-align:middle;">'; const ballImg = isMaxed ? IMG_RED : IMG_BLUE; for (let i = 0; i < val; i++) { html += `<img src="${ballImg}" style="margin-right:1px; width:12px; height:10px;">`; } html += '</div>'; img.addClass('sl-done').hide().after(html); // Sadece orijinal yetenek toplarını gizle, diğer ikonlara (YP/DP ve Gözlemci Yıldızları) dokunma // Orijinal toplar genellikle img.skill sınıfına sahiptir veya src'sinde top/ball geçer img.parent().find('img').each(function () { const $this = $(this); // İşlenmiş toplarımızı atla if ($this.closest('.mz-processed-ball').length > 0) return; // YP/DP ikonlarını ve STARLARI atla const src = ($this.attr('src') || '').toLowerCase(); // Korunacak ikonlar: // 1. Antrenman YP/DP: bar_, trend // 2. Gözlemci Yıldızları: s1.gif, s2.gif, ... veya gm_ ön ekli classlar (bazen img src de olabilir) if (src.includes('bar_') || src.includes('trend') || src.includes('/s1.gif') || src.includes('/s2.gif') || src.includes('/s3.gif') || src.includes('/s4.gif') || src.includes('/s5.gif') || src.includes('star') || src.includes('scout')) return; // Diğer her şeyi (muhtemelen eski toplar) gizle $this.hide(); }); }); }); } let isProcessing = false; async function safeProcess() { if (isProcessing) return; isProcessing = true; try { process(); } finally { isProcessing = false; } } if ((window.location.href.includes('p=players') || window.location.href.includes('p=shortlist'))) { setTimeout(process, 1500); // Dinamik yüklemeler için setInterval (JANK önleyici) setInterval(() => { if (document.querySelector('.playerContainer')) { safeProcess(); } }, 1500); } } /**************************************************************************************** * * * ANA YÖNLENDİRİCİ (MASTER ROUTER) - HATA YAKALAMA VE FİKSTÜR DESTEĞİ İLE GÜNCELLENDİ * * * ****************************************************************************************/ function masterRouter() { if (typeof $ === 'undefined' || typeof $.fn.on === 'undefined') { console.log("SUITE: jQuery is not available yet. Waiting..."); setTimeout(masterRouter, 200); return; } initializeSellTaxCalculator(); try { // Global menü her zaman çalışmalı initializeGlobalInfoMenu(); } catch (error) { console.error(`[MZone Advanced Script] 'Global Info Menu' modülü başlatılırken bir hata oluştu:`, error); } const params = new URLSearchParams(window.location.search); const p_param = params.get('p'); const sub_param = params.get('sub'); const type_param = params.get('type'); const scriptName = GM_info.script.name; function runModule(name, initFunction) { try { console.log(`SUITE: Initializing ${name}...`); initFunction(); } catch (error) { // console.error([name] + " modülü başlatılırken bir hata oluştu: " + error); console.error(`[${scriptName}] '${name}' modülü başlatılırken bir hata oluştu:`, error); } } const leagueTablePages = ['league', 'friendlyseries', 'private_cup', 'cup']; if (scriptSettings.leagueTable && leagueTablePages.includes(p_param)) { runModule("Advanced League Table", initializeLeagueTableScript); } const playerStatsPages_p = ['players', 'player']; const playerStatsPages_sub = ['played', 'result', 'stats']; if ((scriptSettings.playerStats || scriptSettings.retirementIndicator) && (playerStatsPages_p.includes(p_param) || (p_param === 'match' && playerStatsPages_sub.includes(sub_param)))) { if (scriptSettings.playerStats) { runModule("Player Stats", initializePlayerStatsScript); } if (scriptSettings.retirementIndicator && p_param === 'players') { runModule("Retirement Indicator", initializeRetirementIndicatorScript); } } // FİKSTÜR ELO MODÜLÜ if (scriptSettings.fixtureElo && p_param === 'match' && sub_param === 'scheduled') { // owned by factual-insights } if (scriptSettings.playoffPredictor && p_param === 'league' && type_param === 'senior') { runModule("Play-off Predictor", initializePlayoffPredictorScript); } if ((scriptSettings.shortlistFilter || scriptSettings.transferTracker) && p_param === 'shortlist') { if (scriptSettings.shortlistFilter) runModule("Shortlist Filter", initializeShortlistFilterScript); if (scriptSettings.transferTracker) runModule("Transfer Tracker UI", initializeTransferTrackerScript); } if (scriptSettings.statisticsSummary && p_param === 'statistics') { runModule("Statistics Summary", initializeStatisticsSummaryScript); } if (p_param === 'rank' && sub_param === 'userrank') { runModule("Ranking Page Tweaks", initializeRankingPageTweaks); } if (scriptSettings.matchFinder && p_param === 'match' && (sub_param === 'played' || sub_param === 'scheduled')) { runModule("Advanced Match Finder", initializeMatchFinderScript); } // MESSENGER, GHOST ROBOT, FRIENDLY MATCH if (scriptSettings.messengerTools && p_param === 'messenger') { runModule("Messenger Enhancements", initializeMessengerEnhancements); } if (scriptSettings.ghostRobot && p_param === 'tactics') { // disabled: bulk tactic writes are outside mzcopilot scope } if (scriptSettings.friendlyMatchAuto) { // disabled: unattended friendly proposals are outside mzcopilot scope } if (scriptSettings.linkEnhancements) { if ((p_param === 'league') || (p_param === 'match' && sub_param === 'result')) { runModule("Link Enhancements", initializeLinkEnhancements); } } if (scriptSettings.transferScoutFilter && p_param === 'transfer') { runModule("Transfer Scout Filter", initializeTransferScoutFilterScript); } // YETENEK RENKLENDİRME MODÜLLERİ (AYRI FONKSİYONLAR) if (scriptSettings.maxedBallColoring) { if (p_param === 'transfer') { runModule("Transfer Market Coloring", initializeTransferMarketColoring); } else if (p_param === 'shortlist') { runModule("Shortlist Coloring", initializeShortlistColoring); } else if (p_param === 'players' || p_param === 'tactics' || p_param === 'training_camp') { runModule("Kadro/Taktik Coloring", initializeMaxedBallColoringScript); } } // YP/DP Metin Renklendirme Modülleri (Mevcut yapıyı koruyoruz) if (p_param === 'players' && params.has('pid')) { runModule("Profile Text Colorizer", initializeSinglePlayerScoutColoring); } // Residency and Transfer Info (Supports Players, Transfer, and Shortlist pages) if (scriptSettings.residencyInfo && (p_param === 'players' || p_param === 'transfer' || p_param === 'shortlist')) { runModule("Residency and Transfer Info", initializeResidencyModule); } if (p_param === 'shortlist') { runModule("Shortlist Text Colorizer", initializeShortlistTextColoring); } // YP/DP yetenek bazlı renklendirmeyi de ekleyelim (Bölüm 17) const isMainPlayersPage = p_param === 'players' && !params.has('pid'); if (scriptSettings.skillColoring && (p_param === 'transfer' || p_param === 'shortlist' || isMainPlayersPage || p_param === 'training_camp' || p_param === 'tactics')) { runModule("Skill Coloring (Van Style)", initializeSkillColoringScript); } if (p_param === 'transfer') { runModule("Transfer Market Text Colorizer", initializeTransferMarketTextColoring); } // Scout popup coloring and summary are owned by legacy-player-advanced. if (scriptSettings.trainingPercentage) { runModule("Training Percentage", initializeTrainingPercentageScript); } } (async () => { await loadScriptSettings(); initializeGlobalScoutMatchListener(); masterRouter(); })(); /**************************************************************************************** * * * BÖLÜM 11: FİKSTÜR SAYFASI ELO MODÜLÜ (Doğrudan Tetiklemeli) - DÜZELTİLMİŞ VERSİYON * * * ****************************************************************************************/ function runFixtureEloModule() { // Sadece fikstür sayfasında çalışmasını garantile const params = new URLSearchParams(window.location.search); if (params.get('p') !== 'match' || params.get('sub') !== 'scheduled') { return; } console.log("SUITE: Fikstür sayfası algılandı. ELO modülü başlatılıyor."); // Maç listesinin yüklenmesini bekle (AJAX uyumluluğu için) const waitForFixtures = setInterval(function () { const matchEntries = $('dd.odd'); // HTML koduna göre doğru seçici if (matchEntries.length > 0) { clearInterval(waitForFixtures); console.log(`SUITE: ${matchEntries.length} maç satırı bulundu. ELO verileri çekiliyor.`); const myTeamId = $('#challenge-team-id-1').val() || null; if (!myTeamId) { console.warn("SUITE: Kullanıcı Takım ID'si bulunamadı. Kendi takımınızın ELO'su gösterilemeyebilir."); } const teamIds = new Set(); // 1. ADIM: Sayfadaki tüm takım ID'lerini topla matchEntries.each(function () { $(this).find('a.clippable[href*="p=team"]').each(function () { const teamLink = $(this); const href = teamLink.attr('href'); let tid = null; if (href.includes('&tid=')) { const url = new URL(href, window.location.origin); tid = url.searchParams.get('tid'); } else if (myTeamId) { tid = myTeamId; } if (tid) { teamIds.add(tid); } }); }); if (teamIds.size === 0) return; // 2. ADIM: API isteği için ID'leri hazırla const linkIds = Array.from(teamIds).map((id, i) => `&idEquipo${i}=${id}`).join(''); // 3. ADIM: statsxente.com API'sine istek gönder GM_xmlhttpRequest({ method: "GET", // DİKKAT: Diğer betikle aynı API endpoint'ini kullanıyoruz url: `https://statsxente.com/MZ1/Functions/tamper_elo_values.php?sport=soccer${linkIds}`, headers: { "Content-Type": "application/json" }, onload: function (response) { try { const data = JSON.parse(response.responseText); const eloData = data.teams; if (!eloData) return; // 4. ADIM: ELO puanlarını ilgili takımların altına ekle matchEntries.each(function () { const matchRow = $(this); // ▼▼▼ DEĞİŞİKLİK BURADA BAŞLIYOR ▼▼▼ // KATEGORİYİ BELİRLEMEK İÇİN DOĞRU YÖNTEM let eloCategory = 'SENIOR'; // Varsayılan kategori // Sadece linki değil, tüm metni içeren kapsayıcıyı buluyoruz. const categoryContainer = matchRow.find('.responsive-hide.match-reference-text-wrapper'); if (categoryContainer.length > 0) { // Artık tüm metni ('U18 Dünya Ligi div5.110') kontrol ediyoruz. const categoryText = categoryContainer.text().toLowerCase(); if (categoryText.includes('u23')) { eloCategory = 'U23'; } else if (categoryText.includes('u21')) { eloCategory = 'U21'; } else if (categoryText.includes('u18')) { eloCategory = 'U18'; } } // Takımları bul ve ELO'larını ekle/güncelle matchRow.find('a.clippable[href*="p=team"]').each(function () { const teamLink = $(this); // Takım adını ve bayrağı içeren .flex-grow-1 elementini buluyoruz const elementContainer = teamLink.closest('.flex-grow-1'); const href = teamLink.attr('href'); let tid = null; if (href.includes('&tid=')) { const url = new URL(href, window.location.origin); tid = url.searchParams.get('tid'); } else if (myTeamId) { tid = myTeamId; } if (!tid) return; const teamEloData = eloData[tid]; if (teamEloData && teamEloData[eloCategory] !== undefined) { const eloScore = Math.round(parseFloat(teamEloData[eloCategory])); // Önce mevcut ELO span'ını bulmaya çalış let eloSpan = elementContainer.find('.fixture-elo-score'); // Eğer span yoksa, oluştur ve ekle if (eloSpan.length === 0) { eloSpan = $('<br><span class="fixture-elo-score"></span>'); elementContainer.append(eloSpan); } // İçeriğini doğru ELO puanı ile güncelle eloSpan.text(`ELO: ${eloScore.toLocaleString('de-DE')}`); } }); // ▲▲▲ DEĞİŞİKLİK BURADA BİTİYOR ▲▲▲ }); } catch (error) { console.error("SUITE: ELO verisi işlenirken hata oluştu:", error); } }, onerror: function (error) { console.error("SUITE: ELO API hatası:", error); } }); } }, 500); // Her yarım saniyede bir kontrol et setTimeout(() => clearInterval(waitForFixtures), 10000); // 10 saniye sonra dur } /**************************************************************************************** * * * BÖLÜM 12: GELİŞMİŞ MAÇ BULUCU (Advanced Match Finder) - YENİ MODÜL (i18n EKLENDİ) * * * ****************************************************************************************/ function initializeMatchFinderScript() { 'use strict'; const $ = jQuery; const i18n = { tr: { teamUserInputLabel: "Takım / Kullanıcı Adı:", teamUserInputPlaceholder: "Tam adı girin...", findMatchesButton: "Maçları Ara", showResultsButton: "Sonuçları Göster", statusSearching: "Arama yapılıyor...", statusNotFound: "Arama sonucu bulunamadı. Lütfen adı kontrol edin.", statusExactMatch: (name) => `Tam eşleşme bulundu: "${name}". İşleniyor...`, statusSingleMatch: (name) => `Tek sonuç bulundu: "${name}". İşleniyor...`, statusMultipleMatches: "Birden fazla sonuç bulundu, lütfen birini seçin:", statusSelected: (name) => `"${name}" seçildi, işleniyor...`, statusScanningProfile: "Kullanıcı profili taranıyor...", statusFetchingMatches: (tid) => `Takım ID (${tid}) ile maç listesi alınıyor...`, statusComplete: (count) => `İşlem tamamlandı. ${count} maç bulundu.`, statusErrorPrefix: "Hata: ", errorSearchComponents: "Sitenin ana arama bileşenleri bulunamadı.", errorInvalidTid: "Seçilen hedeften geçerli bir takım IDsi alınamadı.", errorFetchingList: (status) => `Maç listesi alınamadı (HTTP ${status})`, alertFetchError: (msg) => `Rakip bilgileri alınırken bir hata oluştu: ${msg}`, alertFilterFirst: 'Lütfen incelemek için önce maçları filtreleyin.', modalTitle: (teamName, count) => `'<strong>${teamName}</strong>' Arama Sonucu (${count} Maç)`, filterByResultLabel: "Sonuca Göre Filtrele:", filterAll: "Tümü", filterWins: "Galibiyetler", filterLosses: "Mağlubiyetler", filterDraws: "Beraberlikler", filterByTimeLabel: " Zaman Aralığı:", timeAll: "Tümü (Maksimum)", time1Week: "Son 1 Hafta", time2Weeks: "Son 2 Hafta", filterByTournamentLabel: "Turnuvaya Göre Filtrele:", selectAll: "Tümünü Seç", deselectAll: "Seçimi Kaldır", viewTacticsButton: "Filtrelenmiş Taktikleri Görüntüle", tableHeaderDate: "Tarih", tableHeaderTournament: "Turnuva", tableHeaderOpponent: "Rakip", tableHeaderScore: "Skor", noMatchesFound: "Bu kriterlere uygun maç bulunamadı.", // Taktik İnceleme tooltipProcessing: "İşleniyor...", tooltipImageError: "Resim işlenemedi.", reviewButtonPreparing: "Görseller Hazırlanıyor...", reviewProgress: (current, total) => `${current} / ${total} taktik hazırlandı...`, visualReviewTitle: (count) => `Filtrelenmiş Maç Taktikleri (${count} Maç)`, // Fikstür İkonu analyzeOpponentTitle: "Rakibin maç sonuçlarını analiz et", analyzingOpponentTitle: "Analiz ediliyor...", // Diğer resultTypeTeam: "Takım", resultTypeUser: "Kullanıcı", resultTypeUnknown: "Bilinmiyor", // Maç Filtreleme Anahtar Kelimeleri (Genişletilmiş) excludedMatchTypeKeywords: [ 'Hazırlık Maçı', 'Gözlemci Maçı', 'Hemen Maç Yap', 'Dostluk Ligi', 'Friendly Match', 'Scout Match', 'Instant Match', 'Friendly League', 'Reto', 'Desafío', 'Partido amistoso', 'Liga amistosa', 'Match amical', 'Défi', 'Ligue amicale', 'Träningsmatch', 'Utmaning', 'Kompisliga', 'Challenge' ] }, en: { // UI teamUserInputLabel: "Team / User Name:", teamUserInputPlaceholder: "Enter the full name...", findMatchesButton: "Find Matches", showResultsButton: "Show Results", // Status Messages statusSearching: "Searching...", statusNotFound: "No search results found. Please check the name.", statusExactMatch: (name) => `Exact match found: "${name}". Processing...`, statusSingleMatch: (name) => `Single result found: "${name}". Processing...`, statusMultipleMatches: "Multiple results found, please select one:", statusSelected: (name) => `Selected "${name}", processing...`, statusScanningProfile: "Scanning user profile...", statusFetchingMatches: (tid) => `Fetching match list with Team ID (${tid})...`, statusComplete: (count) => `Process complete. Found ${count} matches.`, statusErrorPrefix: "Error: ", // Error Messages errorSearchComponents: "Could not find the site's main search components.", errorInvalidTid: "Could not retrieve a valid team ID from the selected target.", errorFetchingList: (status) => `Failed to fetch match list (HTTP ${status})`, alertFetchError: (msg) => `An error occurred while fetching opponent data: ${msg}`, alertFilterFirst: 'Please filter the matches to review first.', // Results Modal modalTitle: (teamName, count) => `Search Result for '<strong>${teamName}</strong>' (${count} Matches)`, filterByResultLabel: "Filter by Result:", filterAll: "All", filterWins: "Wins", filterLosses: "Losses", filterDraws: "Draws", filterByTimeLabel: "Time Range:", timeAll: "All (Maximum)", time1Week: "Last 1 Week", time2Weeks: "Last 2 Weeks", filterByTournamentLabel: "Filter by Tournament:", selectAll: "Select All", deselectAll: "Deselect All", viewTacticsButton: "View Filtered Tactics", tableHeaderDate: "Date", tableHeaderTournament: "Tournament", tableHeaderOpponent: "Opponent", tableHeaderScore: "Score", noMatchesFound: "No matches found for these criteria.", // Tactic Review tooltipProcessing: "Processing...", tooltipImageError: "Could not process image.", reviewButtonPreparing: "Preparing Visuals...", reviewProgress: (current, total) => `${current} / ${total} tactics prepared...`, visualReviewTitle: (count) => `Filtered Match Tactics (${count} Matches)`, // Fixture Icon analyzeOpponentTitle: "Analyze opponent's match results", analyzingOpponentTitle: "Analyzing...", // Other resultTypeTeam: "Team", resultTypeUser: "User", resultTypeUnknown: "Unknown", // Match Filtering Keywords excludedMatchTypeKeywords: ['Friendly Match', 'Scout Match', 'Instant Match', 'Friendly League'] } }; const lang = Core.i18n.lang; const getText = (key, ...args) => { const text = i18n[lang][key] || i18n.en[key]; // Fallback to English if (typeof text === 'function') { return text(...args); } return text; }; // ▲▲▲ DEĞİŞİKLİK 1 SONU ▲▲▲ let allFoundMatches = []; let currentFilteredMatches = []; let currentSearchInput = ''; const tacticImageCache = {}; function addStyles() { GM_addStyle(` #quickSearchResponsePlaceholder { position: fixed !important; top: -9999px !important; left: -9999px !important; opacity: 0 !important; pointer-events: none !important; z-index: -1 !important; } #lmf-tactic-tooltip { display: none; position: absolute; z-index: 10001; background-color: #1C2833; border: 1px solid #566573; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.5); padding: 10px; width: auto; pointer-events: none; } #lmf-tactic-tooltip img { display: block; max-width: 350px; border-radius: 4px; } #lmf-tactic-tooltip .tooltip-loading { font-style: italic; color: #999; padding: 20px; text-align: center; } #lmf-container { position: relative; padding: 15px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 8px; margin: 15px 0; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } #lmf-team-input { padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; width: 300px; font-size: 14px; } #lmf-find-button { padding: 8px 18px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 14px; transition: background-color 0.2s; } #lmf-find-button:disabled { background-color: #aaa; cursor: not-allowed; } #lmf-status { font-style: italic; color: #555; } #lmf-results-modal, #lmf-visual-review-modal { display: none; position: fixed; z-index: 10000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.7); backdrop-filter: blur(4px); font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; } .lmf-modal-content { background: #283747; color: #D5D8DC; margin: 5% auto; padding: 25px 30px; border-top: 4px solid #3498DB; border-radius: 8px; width: 90%; max-width: 950px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); } #lmf-visual-review-modal .lmf-modal-content { max-width: 1400px; } .lmf-modal-close { color: #999; position: absolute; top: 15px; right: 20px; font-size: 28px; font-weight: bold; cursor: pointer; transition: color 0.2s, transform 0.2s; } .lmf-modal-close:hover { color: #fff; transform: scale(1.1); } .lmf-modal-title { font-size: 22px; text-align: center; padding-bottom: 15px; margin-bottom: 20px; border-bottom: 1px solid #4A5568; color: #fff; } .lmf-modal-title strong { color: #3498DB; font-weight: 700; } #lmf-result-filter-pane, #lmf-filter-pane { background-color: #1C2833; padding: 15px; border-radius: 6px; margin-bottom: 15px; } #lmf-result-filter-controls { display: flex; justify-content: space-between; align-items: center; } #lmf-result-filter-controls > div { display: flex; align-items: center; } #lmf-result-filter-controls span { font-weight: bold; font-size: 14px; color: #EAECEE; } #lmf-result-filter-controls label { font-size: 13px; color: #D5D8DC; margin-left: 15px; cursor: pointer; } #lmf-result-filter-controls input[type="radio"] { margin-right: 5px; vertical-align: middle; accent-color: #3498DB; } #lmf-time-filter-group { display: flex; align-items: center; gap: 8px; font-weight: bold; font-size: 14px; color: #EAECEE; } #lmf-time-filter { background-color: #283747; color: #D5D8DC; border: 1px solid #566573; border-radius: 4px; padding: 5px 8px; } #lmf-filter-controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding-bottom: 10px; border-bottom: 1px solid #4A5568; } #lmf-filter-controls > span { font-weight: bold; font-size: 14px; color: #EAECEE; } #lmf-filter-controls button { background: #3498DB; border: none; color: white; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: all 0.2s; } #lmf-filter-controls button:hover { background: #5DADE2; } #lmf-filter-controls #lmf-deselect-all { background: #566573; } #lmf-filter-controls #lmf-deselect-all:hover { background: #808B96; } #lmf-checkbox-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 10px; max-height: 120px; overflow-y: auto; padding: 10px 5px; } .lmf-checkbox-label { display: flex; align-items: center; background-color: #283747; padding: 8px 12px; border-radius: 4px; cursor: pointer; transition: background-color 0.2s; font-size: 13px; color: #D5D8DC; } .lmf-checkbox-label:hover { background-color: #566573; } .lmf-checkbox-label input { margin-right: 10px; width: 15px; height: 15px; accent-color: #3498DB; } #lmf-review-button-container { text-align: center; margin: -5px 0 10px 0; } #lmf-review-button { font-size: 14px; padding: 10px 25px; background-color: #27AE60; border: none; color: white; border-radius: 4px; cursor: pointer; transition: all 0.2s; } #lmf-review-button:disabled { background-color: #999; cursor: not-allowed; } #lmf-review-button:hover:not(:disabled) { background-color: #2ECC71; } #lmf-review-progress-status { text-align: center; color: #F1C40F; padding: 5px 0; height: 20px; font-style: italic; font-weight: bold; } #lmf-results-table { width: 100%; border-collapse: collapse; font-size: 14px; margin-top: 10px; } #lmf-results-table th, #lmf-results-table td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #4A5568; } #lmf-results-table th { background-color: #1C2833; color: #EAECEE; font-size: 12px; text-transform: uppercase; } #lmf-results-table td { color: #D5D8DC; } #lmf-results-table tbody tr:hover { background-color: #4A5568; } .lmf-score-cell { font-weight: bold; color: #fff; cursor: help; } .lmf-score-cell-loss { background-color: rgba(192, 57, 43, 0.4); } .lmf-score-cell-win { background-color: rgba(39, 174, 96, 0.4); } .lmf-score-cell-draw { background-color: rgba(241, 196, 15, 0.4); } #lmf-results-table a { color: #5DADE2; text-decoration: none; font-weight: 600; } #lmf-visual-review-results { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; margin-top: 20px; max-height: 70vh; overflow-y: auto; padding: 10px; } .visual-tactic-item { background-color: #1C2833; border: 1px solid #4A5568; border-radius: 8px; padding: 15px; text-align: center; display: flex; flex-direction: column; align-items: center; } .visual-tactic-item img { width: 100%; max-width: 300px; height: auto; border-radius: 4px; background-color: #006400; } .visual-tactic-info { margin-top: 12px; font-size: 14px; width: 100%; } .visual-tactic-info .opponent { font-weight: bold; color: #EAECEE; } .visual-tactic-info .score { font-size: 16px; font-weight: bold; margin-top: 5px; padding: 4px 10px; border-radius: 4px; display: inline-block; } .score-win { background-color: rgba(39, 174, 96, 0.4); color: #fff; } .score-loss { background-color: rgba(192, 57, 43, 0.4); color: #fff; } .score-draw { background-color: rgba(241, 196, 15, 0.4); color: #fff; } `); } function scrapeAndDisplayAllMatches(teamName, searchContext) { const allMatches = []; let currentDate = ''; // ▼▼▼ DEĞİŞİKLİK 2: Filtreleme için dil-bağımsız anahtar kelimeler kullanılıyor ▼▼▼ const excludedKeywords = getText('excludedMatchTypeKeywords'); searchContext.querySelectorAll('#fixtures-results-list > dd').forEach(dd => { if (dd.classList.contains('group')) { currentDate = dd.textContent.trim(); return; } const scoreEl = dd.querySelector('.score-shown'); if (!scoreEl) return; const homeTeamEl = dd.querySelector('.home-team-column'); const awayTeamEl = dd.querySelector('.away-team-column'); if (!homeTeamEl || !awayTeamEl || (!homeTeamEl.querySelector('strong') && !awayTeamEl.querySelector('strong'))) return; const matchTypeEl = dd.querySelector('.responsive-hide.match-reference-text-wrapper > span'); const matchType = matchTypeEl ? matchTypeEl.textContent.trim() : getText('resultTypeUnknown'); // Maç türü anahtar kelime kontrolü if (excludedKeywords.some(keyword => matchType.includes(keyword))) { return; } const dateParts = currentDate.split('-'); const matchDateObject = new Date(`${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`); const isOurTeamHome = !!homeTeamEl.querySelector('strong'); const scoreParts = scoreEl.textContent.trim().split('-').map(s => parseInt(s.trim(), 10)); if (scoreParts.length !== 2 || isNaN(scoreParts[0]) || isNaN(scoreParts[1])) return; let result = 'Draw'; const ourScore = isOurTeamHome ? scoreParts[0] : scoreParts[1]; const opponentScore = isOurTeamHome ? scoreParts[1] : scoreParts[0]; if (ourScore > opponentScore) result = 'Win'; else if (ourScore < opponentScore) result = 'Loss'; const finalScore = `${ourScore} - ${opponentScore}`; const opponentName = isOurTeamHome ? awayTeamEl.querySelector('.full-name').textContent.trim() : homeTeamEl.querySelector('.full-name').textContent.trim(); allMatches.push({ date: currentDate, matchDateObject: matchDateObject, type: matchType, opponent: opponentName, score: finalScore, link: scoreEl.closest('a').href, isHome: isOurTeamHome, result: result }); }); allFoundMatches = allMatches; currentSearchInput = teamName; displayResultsModal(allFoundMatches, teamName); document.getElementById('lmf-results-modal').style.display = 'block'; if (document.getElementById('lmf-find-button')) { updateMainButtonToToggleModal(teamName); } } function displayResultsModal(matches, teamName) { let modal = document.getElementById('lmf-results-modal'); if (modal) modal.remove(); modal = document.createElement('div'); modal.id = 'lmf-results-modal'; const totalMatches = matches.length; const uniqueTournamentTypes = [...new Set(matches.map(m => m.type))].sort(); const checkboxHTML = uniqueTournamentTypes.map(type => `<label class="lmf-checkbox-label"><input type="checkbox" class="lmf-type-checkbox" value="${type}" checked> ${type}</label>`).join(''); // ▼▼▼ DEĞİŞİKLİK 3: Modal içindeki tüm metinler getText() ile değiştirildi ▼▼▼ modal.innerHTML = ` <div class="lmf-modal-content"> <span class="lmf-modal-close">×</span> <div id="lmf-modal-dynamic-title" class="lmf-modal-title">${getText('modalTitle', teamName, totalMatches)}</div> <div id="lmf-result-filter-pane"> <div id="lmf-result-filter-controls"> <div> <span>${getText('filterByResultLabel')}</span> <label><input type="radio" name="resultFilter" value="All" checked> ${getText('filterAll')}</label> <label><input type="radio" name="resultFilter" value="Win"> ${getText('filterWins')}</label> <label><input type="radio" name="resultFilter" value="Loss"> ${getText('filterLosses')}</label> <label><input type="radio" name="resultFilter" value="Draw"> ${getText('filterDraws')}</label> </div> <div id="lmf-time-filter-group"> <span>${getText('filterByTimeLabel')}</span> <select id="lmf-time-filter"> <option value="all">${getText('timeAll')}</option> <option value="1week">${getText('time1Week')}</option> <option value="2weeks">${getText('time2Weeks')}</option> </select> </div> </div> </div> <div id="lmf-filter-pane"> <div id="lmf-filter-controls"> <span>${getText('filterByTournamentLabel')}</span> <div> <button id="lmf-select-all">${getText('selectAll')}</button> <button id="lmf-deselect-all" style="margin-left: 5px;">${getText('deselectAll')}</button> </div> </div> <div id="lmf-checkbox-container">${checkboxHTML}</div> </div> <div id="lmf-review-button-container"> <button id="lmf-review-button">${getText('viewTacticsButton')}</button> </div> <div id="lmf-review-progress-status"></div> <table id="lmf-results-table"> <thead> <tr> <th>${getText('tableHeaderDate')}</th> <th>${getText('tableHeaderTournament')}</th> <th>${getText('tableHeaderOpponent')}</th> <th>${getText('tableHeaderScore')}</th> </tr> </thead> <tbody>${generateTableRowsHTML(matches)}</tbody> </table> </div>`; document.body.appendChild(modal); modal.querySelector('.lmf-modal-close').onclick = () => { modal.style.display = 'none'; }; modal.onclick = (e) => { if (e.target === modal) modal.style.display = 'none'; }; document.querySelectorAll('.lmf-type-checkbox, input[name="resultFilter"], #lmf-time-filter').forEach(el => el.addEventListener('change', handleFilterChange)); document.getElementById('lmf-select-all').addEventListener('click', () => toggleAllCheckboxes(true)); document.getElementById('lmf-deselect-all').addEventListener('click', () => toggleAllCheckboxes(false)); document.getElementById('lmf-review-button').addEventListener('click', runVisualReview); const tableBody = modal.querySelector('#lmf-results-table tbody'); tableBody.addEventListener('mouseover', handleScoreHover); tableBody.addEventListener('mouseout', handleScoreMouseOut); handleFilterChange(); } function handleFilterChange() { const selectedResult = document.querySelector('input[name="resultFilter"]:checked').value; const checkedBoxes = document.querySelectorAll('.lmf-type-checkbox:checked'); const selectedTypes = Array.from(checkedBoxes).map(cb => cb.value); const selectedTimeRange = document.getElementById('lmf-time-filter').value; let filteredMatches = allFoundMatches; if (selectedResult !== 'All') { filteredMatches = filteredMatches.filter(match => match.result === selectedResult); } filteredMatches = filteredMatches.filter(match => selectedTypes.includes(match.type)); if (selectedTimeRange !== 'all') { const now = new Date(); const daysToSubtract = selectedTimeRange === '1week' ? 7 : 14; const cutoffDate = new Date(); cutoffDate.setDate(now.getDate() - daysToSubtract); cutoffDate.setHours(0, 0, 0, 0); filteredMatches = filteredMatches.filter(match => match.matchDateObject >= cutoffDate); } currentFilteredMatches = filteredMatches; document.querySelector('#lmf-results-table tbody').innerHTML = generateTableRowsHTML(filteredMatches); const titleEl = document.getElementById('lmf-modal-dynamic-title'); if (titleEl) { titleEl.innerHTML = getText('modalTitle', currentSearchInput, filteredMatches.length); } } function createTacticTooltip() { if (document.getElementById('lmf-tactic-tooltip')) return; const tooltip = document.createElement('div'); tooltip.id = 'lmf-tactic-tooltip'; document.body.appendChild(tooltip); } function getModifiedTacticImage(imageUrl, colorToReplace, callback) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.crossOrigin = 'Anonymous'; img.onload = () => { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; const targetRgb = colorToReplace === 'Sarı' ? [255, 255, 0] : [0, 0, 0]; const replacementRgb = [0, 191, 255]; for (let i = 0; i < data.length; i += 4) { const redDiff = Math.abs(data[i] - targetRgb[0]); const greenDiff = Math.abs(data[i + 1] - targetRgb[1]); const blueDiff = Math.abs(data[i + 2] - targetRgb[2]); const tolerance = 30; if (redDiff < tolerance && greenDiff < tolerance && blueDiff < tolerance) { data[i] = replacementRgb[0]; data[i + 1] = replacementRgb[1]; data[i + 2] = replacementRgb[2]; } } ctx.putImageData(imageData, 0, 0); callback(canvas.toDataURL()); }; img.onerror = () => { callback(null); }; img.src = imageUrl; } function handleScoreHover(e) { const scoreCell = e.target.closest('.lmf-score-cell'); if (!scoreCell) return; const isHome = scoreCell.dataset.ishome === 'true'; const ourColorName = isHome ? 'Sarı' : 'Siyah'; const matchIdLink = scoreCell.querySelector('a').href; const matchIdMatch = matchIdLink.match(/match_id=(\d+)|mid=(\d+)/); if (!matchIdMatch) return; const matchId = matchIdMatch[1] || matchIdMatch[2]; const tacticImageUrl = `https://www.managerzone.com/dynimg/pitch.php?match_id=${matchId}`; const cacheKey = `${matchId}-${ourColorName}`; const tooltip = document.getElementById('lmf-tactic-tooltip'); const updateTooltipPosition = (event) => { tooltip.style.visibility = 'hidden'; tooltip.style.display = 'block'; const tooltipRect = tooltip.getBoundingClientRect(); let top = event.pageY + 15; let left = event.pageX + 15; if (top + tooltipRect.height > window.innerHeight + window.scrollY) { top = event.pageY - tooltipRect.height - 15; } if (left + tooltipRect.width > window.innerWidth + window.scrollX) { left = event.pageX - tooltipRect.width - 15; } tooltip.style.top = `${top}px`; tooltip.style.left = `${left}px`; tooltip.style.visibility = 'visible'; }; const displayImage = (imageUrl) => { const rotationStyle = !isHome ? 'style="transform: rotate(180deg);"' : ''; tooltip.innerHTML = `<img src="${imageUrl}" ${rotationStyle} alt="Processed Tactic Board">`; updateTooltipPosition(e); }; if (tacticImageCache[cacheKey]) { displayImage(tacticImageCache[cacheKey]); } else { tooltip.innerHTML = `<div class="tooltip-loading">${getText('tooltipProcessing')}</div>`; updateTooltipPosition(e); getModifiedTacticImage(tacticImageUrl, ourColorName, (modifiedDataUrl) => { if (modifiedDataUrl) { tacticImageCache[cacheKey] = modifiedDataUrl; if (scoreCell.matches(':hover')) displayImage(modifiedDataUrl); } else { tooltip.innerHTML = `<div class="tooltip-loading">${getText('tooltipImageError')}</div>`; updateTooltipPosition(e); } }); } } function handleScoreMouseOut() { const tooltip = document.getElementById('lmf-tactic-tooltip'); if (tooltip) tooltip.style.display = 'none'; } async function runVisualReview() { const reviewBtn = document.getElementById('lmf-review-button'); const statusEl = document.getElementById('lmf-review-progress-status'); if (!currentFilteredMatches || currentFilteredMatches.length === 0) { alert(getText('alertFilterFirst')); return; } reviewBtn.disabled = true; reviewBtn.textContent = getText('reviewButtonPreparing'); statusEl.textContent = getText('reviewProgress', 0, currentFilteredMatches.length); const matchPromises = currentFilteredMatches.map((match, index) => { return new Promise(async (resolve) => { const matchIdMatch = match.link.match(/match_id=(\d+)|mid=(\d+)/); if (!matchIdMatch) return resolve(null); const matchId = matchIdMatch[1] || matchIdMatch[2]; const tacticImageUrl = `https://www.managerzone.com/dynimg/pitch.php?match_id=${matchId}`; const ourColor = match.isHome ? 'Sarı' : 'Siyah'; const cacheKey = `${matchId}-${ourColor}`; const processImage = (imageUrl) => { getModifiedTacticImage(imageUrl, ourColor, (modifiedDataUrl) => { if (modifiedDataUrl) { tacticImageCache[cacheKey] = modifiedDataUrl; statusEl.textContent = getText('reviewProgress', index + 1, currentFilteredMatches.length); resolve({ ...match, imageDataUrl: modifiedDataUrl }); } else { resolve(null); } }); }; if (tacticImageCache[cacheKey]) { statusEl.textContent = getText('reviewProgress', index + 1, currentFilteredMatches.length); resolve({ ...match, imageDataUrl: tacticImageCache[cacheKey] }); } else { processImage(tacticImageUrl); } }); }); const reviewedMatches = (await Promise.all(matchPromises)).filter(Boolean); statusEl.textContent = ''; reviewBtn.disabled = false; reviewBtn.textContent = getText('viewTacticsButton'); displayVisualReviewModal(reviewedMatches); } function displayVisualReviewModal(matches) { let modal = document.getElementById('lmf-visual-review-modal'); if (modal) modal.remove(); const resultsHTML = matches.map(match => { const rotationStyle = !match.isHome ? 'style="transform: rotate(180deg);"' : ''; const scoreClass = `score-${match.result.toLowerCase()}`; return `<div class="visual-tactic-item"><img src="${match.imageDataUrl}" ${rotationStyle} alt="Tactic Board"><div class="visual-tactic-info"><p class="opponent">vs ${match.opponent}</p><p class="score ${scoreClass}">${match.score}</p><p class="tournament-type" style="font-size: 12px; color: #99A3A4; margin-top: 4px;">${match.type}</p></div></div>`; // <-- YENİ EKLENEN SATIR BURADA }).join(''); modal = document.createElement('div'); modal.id = 'lmf-visual-review-modal'; modal.innerHTML = ` <div class="lmf-modal-content"> <span class="lmf-modal-close">×</span> <div class="lmf-modal-title">${getText('visualReviewTitle', matches.length)}</div> <div id="lmf-visual-review-results">${resultsHTML}</div> </div>`; document.body.appendChild(modal); modal.style.display = 'block'; modal.querySelector('.lmf-modal-close').onclick = () => { modal.style.display = 'none'; }; modal.onclick = (e) => { if (e.target === modal) modal.style.display = 'none'; }; } function generateTableRowsHTML(matches) { if (!matches || matches.length === 0) return `<tr><td colspan="4" style="text-align:center; padding: 20px;">${getText('noMatchesFound')}</td></tr>`; return matches.map(m => { const scoreClass = `lmf-score-cell-${m.result.toLowerCase()}`; return `<tr><td>${m.date}</td><td>${m.type}</td><td>${m.opponent}</td><td class="lmf-score-cell ${scoreClass}" data-ishome="${m.isHome}"><a href="${m.link}" target="_blank">${m.score}</a></td></tr>`; }).join(''); } function createInitialUI() { const container = document.createElement('div'); container.id = 'lmf-container'; container.innerHTML = ` <label for="lmf-team-input">${getText('teamUserInputLabel')}</label> <input type="text" id="lmf-team-input" placeholder="${getText('teamUserInputPlaceholder')}"> <button id="lmf-find-button">${getText('findMatchesButton')}</button> <span id="lmf-status"></span> <div id="lmf-search-results" style="display:none;"></div>`; const targetArea = document.querySelector('#results-fixtures-header'); if (targetArea) { targetArea.insertAdjacentElement('afterend', container); document.getElementById('lmf-find-button').addEventListener('click', startSearch); } } function startSearch() { const searchInput = document.getElementById('lmf-team-input').value.trim(); if (!searchInput) return; const button = document.getElementById('lmf-find-button'); const statusEl = document.getElementById('lmf-status'); const resultsContainer = document.getElementById('lmf-search-results'); const quickSearchInput = document.getElementById('quickSearchField'); const searchResponseContainer = document.getElementById('quickSearchResponseContainer'); if (!quickSearchInput || !searchResponseContainer) { statusEl.textContent = getText('errorSearchComponents'); return; } button.disabled = true; statusEl.textContent = getText('statusSearching'); resultsContainer.innerHTML = ''; resultsContainer.style.display = 'none'; const observer = new MutationObserver((mutations, obs) => { obs.disconnect(); const links = Array.from(searchResponseContainer.querySelectorAll('a')); if (links.length === 0) { statusEl.textContent = getText('statusNotFound'); button.disabled = false; return; } const exactMatches = links.filter(link => link.textContent.trim().toLowerCase() === searchInput.toLowerCase()); if (exactMatches.length > 0) { let targetLink = exactMatches.find(link => link.href.includes('&tid=')) || exactMatches[0]; statusEl.textContent = getText('statusExactMatch', targetLink.textContent.trim()); processSelection(targetLink.href, targetLink.textContent.trim()); return; } if (links.length === 1) { let targetLink = links[0]; statusEl.textContent = getText('statusSingleMatch', targetLink.textContent.trim()); processSelection(targetLink.href, targetLink.textContent.trim()); return; } statusEl.textContent = getText('statusMultipleMatches'); links.forEach(link => { const href = link.href; const name = link.textContent.trim(); let type = href.includes('&tid=') ? getText('resultTypeTeam') : (href.includes('&uid=') ? getText('resultTypeUser') : getText('resultTypeUnknown')); const resultDiv = document.createElement('div'); resultDiv.innerHTML = `<span>${name}</span> <span class="result-type">${type}</span>`; resultDiv.addEventListener('click', () => { resultsContainer.style.display = 'none'; statusEl.textContent = getText('statusSelected', name); button.disabled = true; processSelection(href, name); }); resultsContainer.appendChild(resultDiv); }); resultsContainer.style.display = 'block'; button.disabled = false; }); observer.observe(searchResponseContainer, { childList: true, subtree: true }); quickSearchInput.value = searchInput; quickSearchInput.dispatchEvent(new Event('focus', { bubbles: true })); quickSearchInput.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true })); } async function processSelection(url, teamNameFromLink) { const button = document.getElementById('lmf-find-button'); const statusEl = document.getElementById('lmf-status'); let teamId = null; let finalTeamName = teamNameFromLink || document.getElementById('lmf-team-input').value.trim(); try { if (url.includes('&tid=')) { const match = url.match(/tid=(\d+)/); if (match) teamId = match[1]; } else if (url.includes('&uid=')) { statusEl.textContent = getText('statusScanningProfile'); const userMatch = url.match(/uid=(\d+)/); if (userMatch) { const userId = userMatch[1]; const profileUrl = `https://www.managerzone.com/?p=profile&uid=${userId}`; const profileResponse = await fetch(profileUrl); const profileHtml = await profileResponse.text(); const profileDoc = new DOMParser().parseFromString(profileHtml, 'text/html'); const profileTeamLink = profileDoc.querySelector('a[href*="p=team&tid="]'); if (profileTeamLink) { const teamMatch = profileTeamLink.href.match(/tid=(\d+)/); if (teamMatch) teamId = teamMatch[1]; } } } if (!teamId) { throw new Error(getText('errorInvalidTid')); } await fetchAndScrapeMatches(teamId, finalTeamName); } catch (error) { statusEl.textContent = getText('statusErrorPrefix') + error.message; button.disabled = false; } } async function fetchAndScrapeMatches(teamId, teamName) { const statusEl = document.getElementById('lmf-status'); if (statusEl) statusEl.textContent = getText('statusFetchingMatches', teamId); try { const matchesUrl = `https://www.managerzone.com/?p=match&sub=played&tid=${teamId}&limit=max`; const matchesResponse = await fetch(matchesUrl); if (!matchesResponse.ok) throw new Error(getText('errorFetchingList', matchesResponse.status)); const matchesHtml = await matchesResponse.text(); const matchesDoc = new DOMParser().parseFromString(matchesHtml, 'text/html'); scrapeAndDisplayAllMatches(teamName, matchesDoc); } catch (error) { if (statusEl) { statusEl.textContent = getText('statusErrorPrefix') + error.message; document.getElementById('lmf-find-button').disabled = false; } else { console.error('MZ Advanced Match Finder Error:', error); alert(getText('alertFetchError', error.message)); } } } function updateMainButtonToToggleModal(teamName) { const button = document.getElementById('lmf-find-button'); const statusEl = document.getElementById('lmf-status'); const modal = document.getElementById('lmf-results-modal'); if (!button || !statusEl || !modal) return; button.textContent = getText('showResultsButton'); statusEl.textContent = getText('statusComplete', allFoundMatches.length); button.disabled = false; button.removeEventListener('click', startSearch); button.addEventListener('click', () => { modal.style.display = 'block'; }); } function toggleAllCheckboxes(shouldBeChecked) { document.querySelectorAll('.lmf-type-checkbox').forEach(cb => cb.checked = shouldBeChecked); handleFilterChange(); } function initializeFixturesPage() { const analysisIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="cursor: pointer;" title="${getText('analyzeOpponentTitle')}"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path><line x1="12" y1="3" x2="12" y2="22"></line><line x1="12" y1="12" x2="18" y2="9"></line><line x1="6" y1="9" x2="12" y2="12"></line></svg>`; const matchRows = document.querySelectorAll('#fixtures-results-list > dd:not(.group)'); matchRows.forEach(row => { const opponentLink = row.querySelector('a.clippable:not(:has(strong))'); if (opponentLink) { const opponentName = opponentLink.querySelector('.full-name').textContent.trim(); const opponentUrl = opponentLink.href; const tidMatch = opponentUrl.match(/tid=(\d+)/); if (tidMatch) { const opponentTid = tidMatch[1]; const iconWrapper = document.createElement('span'); iconWrapper.innerHTML = analysisIconSVG; iconWrapper.style.marginLeft = '8px'; iconWrapper.style.display = 'inline-flex'; iconWrapper.style.alignItems = 'center'; iconWrapper.style.verticalAlign = 'middle'; iconWrapper.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const svgIcon = iconWrapper.firstElementChild; svgIcon.style.color = '#e74c3c'; svgIcon.style.transform = 'scale(1.2)'; svgIcon.setAttribute('title', getText('analyzingOpponentTitle')); fetchAndScrapeMatches(opponentTid, opponentName).finally(() => { svgIcon.style.color = 'currentColor'; svgIcon.style.transform = 'scale(1)'; svgIcon.setAttribute('title', getText('analyzeOpponentTitle')); }); }); opponentLink.insertAdjacentElement('afterend', iconWrapper); } } }); } function main() { addStyles(); createTacticTooltip(); if (window.location.href.includes('sub=scheduled')) { initializeFixturesPage(); } else if (window.location.href.includes('sub=played')) { createInitialUI(); } } main(); } /**************************************************************************************** * * * BÖLÜM 13: EMEKLİLİK GÖSTERGESİ (Retirement Indicator) - v11 (AJAX Uyumlu) * * * ****************************************************************************************/ function initializeRetirementIndicatorScript() { 'use strict'; // --- Dil Desteği (i18n) --- const i18n = { tr: { retireWarning: "Bu oyuncu sezon sonunda emekli olacak", canExtendRetirement: "Bu oyuncunun emekliliği 1 sezon ertelenebilir", }, en: { retireWarning: "This player will retire at the end of the season", canExtendRetirement: "This player's retirement can be postponed for 1 season", } }; const lang = Core.i18n.lang; const getText = (key) => i18n[lang][key] || i18n.en[key]; const RETIREMENT_DATA_KEY = 'mz_retirement_data_v2'; // Bu fonksiyon, oyuncu profilleri sayfasında çalışarak emeklilik verilerini depolar. async function scanAndStoreRetirementData() { const retirementData = {}; const playerContainers = document.querySelectorAll('.playerContainer'); playerContainers.forEach(container => { const retireDiv = container.querySelector('.dg_playerview_retire'); if (retireDiv) { const pidSpan = container.querySelector('span.player_id_span'); if (pidSpan && pidSpan.textContent) { const pid = pidSpan.textContent.trim(); let isExtendable = false; const extendIconContainer = container.querySelector('.view_extend_career'); if (extendIconContainer && !extendIconContainer.closest('.semi_transparent')) { isExtendable = true; } retirementData[pid] = { extendable: isExtendable }; } } }); await GM_setValue(RETIREMENT_DATA_KEY, JSON.stringify(retirementData)); console.log(`[Emeklilik Göstergesi] Tarama tamamlandı. ${Object.keys(retirementData).length} emekli oyuncu durumu kaydedildi.`); } /** * Kadro özet sayfasındaki tabloya emeklilik ve kariyer uzatma ikonlarını ekler. * Bu fonksiyon artık hem sayfa ilk yüklendiğinde hem de AJAX sonrası çağrılacak. */ async function displayRetirementIcons() { // Stil kuralını her seferinde eklemek sorun yaratmaz ve tutarlılığı garantiler. GM_addStyle(` #playerAltViewTable td, .alt-view-table-mobile td { vertical-align: middle !important; } `); const retirementDataJSON = await GM_getValue(RETIREMENT_DATA_KEY, '{}'); const retirementData = JSON.parse(retirementDataJSON); if (Object.keys(retirementData).length === 0) { return; // Gösterilecek veri yoksa çık. } // Hem masaüstü hem mobil tabloyu hedef alıyoruz. const tableRows = document.querySelectorAll('#playerAltViewTable tbody tr, .alt-view-table-mobile tbody tr'); tableRows.forEach(row => { const checkbox = row.querySelector('input.snapshot__check-player'); let pid = checkbox ? checkbox.dataset.playerId : null; if (!pid) { const playerLinkEl = row.querySelector('a[href*="&pid="]'); if (playerLinkEl) { const urlParams = new URLSearchParams(new URL(playerLinkEl.href).search); pid = urlParams.get('pid'); } } if (pid && retirementData[pid]) { const nameCell = row.cells[1] || row.cells[0]; // Masaüstü için 1, mobil için 0. hücre const playerLink = nameCell ? nameCell.querySelector('a') : null; // Eğer oyuncu linki varsa ve daha önce ikon eklenmemişse devam et. if (playerLink && !nameCell.querySelector('.retirement-indicator-container')) { const iconContainer = document.createElement('span'); iconContainer.className = 'retirement-indicator-container'; iconContainer.style.display = 'inline-flex'; iconContainer.style.alignItems = 'center'; iconContainer.style.gap = '4px'; iconContainer.style.marginRight = '6px'; iconContainer.style.verticalAlign = 'middle'; const warningIcon = document.createElement('i'); warningIcon.className = 'fa fa-exclamation-triangle'; warningIcon.style.fontSize = '14px'; warningIcon.style.color = '#e60000'; warningIcon.style.cursor = 'help'; warningIcon.style.lineHeight = '1'; warningIcon.title = getText('retireWarning'); iconContainer.appendChild(warningIcon); if (retirementData[pid].extendable) { const heartContainer = document.createElement('span'); heartContainer.className = 'player_icon_placeholder view_extend_career'; heartContainer.title = getText('canExtendRetirement'); heartContainer.style.cursor = 'help'; heartContainer.style.margin = '0'; heartContainer.style.padding = '0'; heartContainer.style.display = 'inline-flex'; heartContainer.style.alignItems = 'center'; const heartIcon = document.createElement('i'); heartIcon.className = 'fa-duotone fa-heart-pulse extend-career-icon'; heartIcon.style.fontSize = '14px'; const iconWrapper = document.createElement('span'); iconWrapper.className = 'player_icon_wrapper'; iconWrapper.style.display = 'inline-flex'; iconWrapper.style.alignItems = 'center'; iconWrapper.appendChild(heartIcon); heartContainer.appendChild(iconWrapper); iconContainer.appendChild(heartContainer); } playerLink.before(iconContainer); } } }); console.log(`[Emeklilik Göstergesi] İkonlar başarıyla uygulandı.`); } /** * ▼▼▼ YENİ FONKSİYON: AJAX DEĞİŞİKLİKLERİNİ İZLEYİCİ ▼▼▼ * Bu fonksiyon, tablo içeriği değiştiğinde displayRetirementIcons'ı tekrar tetikler. */ function setupAjaxObserver() { // AJAX Gözlemcisi (JANK önleyici setInterval) setInterval(() => { const targetNode = document.getElementById('squad_summary'); if (!targetNode) return; const players = targetNode.querySelectorAll('.playerContainer, table.player_skills'); const currentHash = players.length + "_" + (players[0]?.innerText.substring(0,10) || ""); if (window._lastRetirementHash === currentHash) return; window._lastRetirementHash = currentHash; displayRetirementIcons(); }, 1500); } // --- Modül Yönlendiricisi --- const params = new URLSearchParams(window.location.search); const sub = params.get('sub'); if (sub === 'alt') { setTimeout(() => { displayRetirementIcons(); }, 500); } else if (!params.has('pid')) { // Diğer oyuncular sayfalarında (profil görünümleri) tarama yapmaya devam ediyoruz. scanAndStoreRetirementData(); } } /**************************************************************************************** * * * BÖLÜM 14: MENAJER SIRALAMASI GELİŞTİRMELERİ (Ranking Page Tweaks) - NİHAİ SÜRÜM * * * ****************************************************************************************/ function initializeRankingPageTweaks() { 'use strict'; // Sadece menajer sıralaması sayfasında çalıştığından emin ol const params = new URLSearchParams(window.location.search); if (params.get('p') !== 'rank' || params.get('sub') !== 'userrank') { return; } const rankingTable = document.getElementById('userRankTable'); if (!rankingTable) { console.warn('[MZone Advanced] Sıralama tablosu bulunamadı.'); return; } // Başlık satırına ikonu ekle (sadece bir kez) const headerRow = rankingTable.querySelector('thead tr'); if (headerRow && headerRow.cells.length > 2 && !headerRow.querySelector('.scout-header')) { const newHeader = document.createElement('th'); newHeader.innerHTML = '<i class="fa fa-user-secret" title="Gözlemci Maçı"></i>'; newHeader.className = 'scout-header'; newHeader.style.width = '25px'; newHeader.style.textAlign = 'center'; headerRow.cells[2].insertAdjacentElement('afterend', newHeader); } // Her takım satırına tıklanabilir gözlemci ikonunu ekle const bodyRows = rankingTable.querySelectorAll('tbody tr'); bodyRows.forEach(row => { // İkonun tekrar eklenmesini önle if (row.querySelector('.scout-cell')) return; const teamLink = row.querySelector('a[href*="p=team&tid="]'); const newCell = document.createElement('td'); newCell.className = 'scout-cell'; newCell.style.textAlign = 'center'; newCell.style.verticalAlign = 'middle'; if (teamLink) { const href = teamLink.getAttribute('href'); const teamIdMatch = href.match(/tid=(\d+)/); if (teamIdMatch && teamIdMatch[1]) { const teamId = teamIdMatch[1]; const buttonId = `scout_button_${teamId}`; // Her buton için benzersiz bir ID // Çalışan butondaki onclick fonksiyonunu doğrudan kopyalıyoruz. // Bu, ManagerZone'un kendi, çalışan kodunu kullanmamızı sağlar. const onClickAttribute = `button_onClick('${buttonId}', "purchaseChallenge(1, 'instantchallenge', '${teamId}')", 'multi_delay', 'account'); return false;`; const scoutLink = document.createElement('a'); scoutLink.href = 'javascript:;'; // Sayfanın üste kaymasını engeller scoutLink.title = getText('rankingScoutTitle'); scoutLink.id = buttonId; // butona ID veriyoruz scoutLink.setAttribute('onclick', onClickAttribute); // onclick olayını doğrudan atıyoruz scoutLink.innerHTML = ` <span class="fa-stack challenge" style="font-size: 0.8em;"> <i class="fa fa-user-secret fa-stack-2x"></i> <i class="fa fa-star fa-stack-1x"></i> </span>`; newCell.appendChild(scoutLink); } } // Yeni hücreyi, mesaj ikonundan sonraki sütuna ekle row.cells[2].insertAdjacentElement('afterend', newCell); }); } /**************************************************************************************** * * * BÖLÜM 15: MESSENGER TOPLU SİLME ARACI (Nihai Sürüm - Tıklama Simülasyonu) * * * ****************************************************************************************/ function initializeMessengerEnhancements() { 'use strict'; // Sadece messenger sayfasında çalışmasını garantile if (new URLSearchParams(window.location.search).get('p') !== 'messenger') { return; } if (typeof waitForKeyElements !== "function") { alert("HATA: MZone Gelişmiş betiğinin bir parçası olan 'waitForKeyElements' kütüphanesi yüklenemedi. Lütfen betiğinizin başındaki @require satırlarını kontrol edin."); return; } const $ = jQuery; // --- Stiller --- GM_addStyle(` #messenger-bulk-controls { display: inline-flex; align-items: center; gap: 10px; float: right; margin-right: 15px; font-size: 12px; line-height: normal; position: relative; top: 2px; } #messenger-bulk-controls a { color: #FFFFFF; text-decoration: none; font-weight: bold; cursor: pointer; } #messenger-bulk-controls a:hover { text-decoration: underline; } #messenger-bulk-controls .delete-btn { background-color: #d9534f; color: white !important; padding: 4px 10px; border-radius: 4px; border: 1px solid #d43f3a; } #messenger-bulk-controls .delete-btn:hover { background-color: #c9302c; text-decoration: none; } #messenger-list .notification { position: relative; padding-left: 35px; } .messenger-convo-checkbox { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; cursor: pointer; z-index: 10; } `); let uiInitialized = false; // --- Arayüz ve Olay Yöneticileri --- function addBulkControls(jNode) { if (uiInitialized) return; const controlsContainer = $('<div id="messenger-bulk-controls"></div>'); const selectAllLink = $(`<a>${getText('messengerSelectAll')}</a>`).on('click', (e) => { e.preventDefault(); toggleCheckboxes(true); }); const deselectAllLink = $(`<a>${getText('messengerDeselectAll')}</a>`).on('click', (e) => { e.preventDefault(); toggleCheckboxes(false); }); const deleteButton = $(`<a class="delete-btn">${getText('messengerDeleteSelected')}</a>`).on('click', (e) => { e.preventDefault(); handleDeleteSelected(); }); controlsContainer.append(selectAllLink, deselectAllLink, deleteButton).insertBefore(jNode); uiInitialized = true; } function addConversationCheckbox(jNode) { if (jNode.find('.messenger-convo-checkbox').length === 0) { const checkbox = $('<input type="checkbox" class="messenger-convo-checkbox">'); checkbox.on('click', (event) => event.stopPropagation()); jNode.prepend(checkbox); } } function toggleCheckboxes(checkedState) { $('.messenger-convo-checkbox').prop('checked', checkedState); } async function handleDeleteSelected() { const selected = $('.messenger-convo-checkbox:checked'); if (selected.length === 0) { alert(getText('messengerNoSelection')); return; } // Bu seferki onay, betiğin kendi onayıdır. Diğerleri çıkmayacak. if (!confirm(getText('messengerConfirmDelete', selected.length))) { return; } const conversationsToDelete = []; selected.each(function () { conversationsToDelete.push($(this).closest('.notification')); }); const deleteButton = $('#msg-delete-selected'); const originalText = deleteButton.length ? deleteButton.text() : ""; if (deleteButton.length) deleteButton.css('pointer-events', 'none'); for (let i = 0; i < conversationsToDelete.length; i++) { const convoElement = conversationsToDelete[i]; if (deleteButton.length) deleteButton.text(getText('messengerDeleting', i + 1, conversationsToDelete.length)); try { // Kullanıcı tıklamalarını taklit eden yeni fonksiyonu çağır await deleteSingleConversationBySimulation(convoElement); // Başarılı olursa ekrandan kaldır convoElement.fadeOut(300, function () { $(this).remove(); }); } catch (error) { console.error(`Bir sohbet silinirken hata oluştu:`, error); convoElement.css('background-color', 'rgba(255, 0, 0, 0.3)'); } await new Promise(resolve => setTimeout(resolve, 500)); // Adımlar arasına biraz bekleme ekle } if (deleteButton.length) deleteButton.text(originalText).css('pointer-events', 'auto'); alert(getText('messengerDeletionComplete')); } // ▼▼▼ TAMAMEN YENİ, KULLANICI EYLEMLERİNİ TAKLİT EDEN SİLME FONKSİYONU ▼▼▼ function deleteSingleConversationBySimulation(convoElement) { return new Promise(async (resolve, reject) => { try { // Adım 1: Silinecek sohbet satırına tıkla convoElement.trigger('click'); await new Promise(r => setTimeout(r, 750)); // Pencerenin yüklenmesini bekle // Adım 2: Sağda açılan aktif sohbet penceresindeki ayarlar (çark) ikonuna tıkla const activeDialog = $('.messenger-dialog.active'); if (!activeDialog.length) throw new Error("Aktif sohbet penceresi bulunamadı."); activeDialog.find('.dialog-menu').trigger('click'); await new Promise(r => setTimeout(r, 250)); // Menünün açılmasını bekle // Adım 3: Açılan menüdeki #messenger-link-remove ID'li "Sohbeti sil" linkine tıkla const deleteLink = $('#messenger-link-remove'); if (!deleteLink.length) throw new Error("'Sohbeti Sil' linki (#messenger-link-remove) bulunamadı."); deleteLink.trigger('click'); // Adım 4: Onay penceresinin ("powerbox") ve "Evet" butonunun çıkmasını bekle const yesButton = await new Promise((res, rej) => { const startTime = Date.now(); const interval = setInterval(() => { const button = $('#powerbox_confirm_ok_button'); if (button.length > 0) { clearInterval(interval); res(button); } if (Date.now() - startTime > 5000) { clearInterval(interval); rej("Onay penceresi ('Evet' butonu) zaman aşımına uğradı."); } }, 100); }); // Adım 5: "Evet" butonuna tıkla ve işlemi bitir yesButton.trigger('click'); await new Promise(r => setTimeout(r, 300)); // Pencerenin kapanması için bekle resolve(); } catch (error) { reject(error); } }); } // ▲▲▲ YENİ SİLME FONKSİYONU SONU ▲▲▲ // --- waitForKeyElements'i Başlat --- waitForKeyElements("#messenger-settings", addBulkControls, true); waitForKeyElements("#messenger-list > div[id^='notification_message_']", addConversationCheckbox); } function waitForKeyElements( selectorTxt, /* Required: The jQuery selector string that specifies the desired element(s). */ actionFunction, /* Required: The code to run when elements are found. It is passed a jNode to the matched element. */ bWaitOnce, /* Optional: If false, will continue to scan for new elements even after the first match is found. */ iframeSelector /* Optional: If set, identifies the iframe to search. */ ) { var targetNodes, btargetsFound; if (typeof iframeSelector == "undefined") targetNodes = $(selectorTxt); else targetNodes = $(iframeSelector).contents() .find(selectorTxt); if (targetNodes && targetNodes.length > 0) { btargetsFound = true; /*--- Found target node(s). Go through each and act if they are new. */ targetNodes.each(function () { var jThis = $(this); var alreadyFound = jThis.data('alreadyFound') || false; if (!alreadyFound) { //--- Call the payload function. var cancelFound = actionFunction(jThis); if (cancelFound) btargetsFound = false; else jThis.data('alreadyFound', true); } }); } else { btargetsFound = false; } //--- Get the timer-control variable for this selector. var controlObj = waitForKeyElements.controlObj || {}; var controlKey = selectorTxt.replace(/[^\w]/g, "_"); var timeControl = controlObj[controlKey]; //--- Now set or clear the timer as appropriate. if (btargetsFound && bWaitOnce && timeControl) { //--- The only condition where we need to clear the timer. clearInterval(timeControl); delete controlObj[controlKey] } else { //--- Set a timer, if needed. if (!timeControl) { timeControl = setInterval(function () { waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector ); }, 300 ); controlObj[controlKey] = timeControl; } } waitForKeyElements.controlObj = controlObj; } /**************************************************************************************** * * * BÖLÜM 16: MZ HAZIRLIK MAÇI OTOMASYONU (Friendly Match Automation) - v10 (6 Saatlik) * * * ****************************************************************************************/ function initializeFriendlyMatchAutomationScript() { 'use strict'; const $ = window.jQuery || jQuery; // ================================================================================= // 1. SABİTLER VE TANIMLAMALAR // ================================================================================= const urlParams = new URLSearchParams(window.location.search); const isWorkerMode = urlParams.get('mz_mode') === 'worker'; const isChallengePage = window.location.href.includes('p=challenges'); const VALID_DAYS = [1, 2, 4, 5, 6]; const KEY_USER_LIST = 'mz_challenge_user_list_v5'; const KEY_SELECTED_USERS = 'mz_challenge_selected_users_v10'; const KEY_SETTINGS = 'mz_otomasyon_ayarlari_v4_dual_tactic'; const KEY_TACTIC_NAMES = 'mz_user_tactic_names_v8'; const KEY_AUTO_STATUS = 'mz_auto_status_v21_6hours'; // Versiyonu değiştirdik // 6 Saat = 6 * 60 * 60 * 1000 milisaniye const INTERVAL_MS = 6 * 60 * 60 * 1000; const DEFAULT_TACTICS = [{ val: 'a', txt: 'ASIL' }, { val: 'b', txt: 'LİG' }, { val: 'c', txt: '5-3-2N' }, { val: 'l', txt: 'Taktik' }]; const GUNLER = getText('dayNames'); // ================================================================================= // 2. YARDIMCI FONKSİYONLAR // ================================================================================= function getPersistentData(key, defaultVal) { try { let val = GM_getValue(key); if (val !== undefined && val !== null) return val; let local = localStorage.getItem(key); if (local !== null) { GM_setValue(key, local); return local; } } catch (e) { console.warn("Storage Error:", e); } return defaultVal; } function setPersistentData(key, value) { try { GM_setValue(key, value); } catch (e) { } } function removePersistentData(key) { try { GM_deleteValue(key); } catch (e) { } } function getQueueData() { return JSON.parse(localStorage.getItem(KEY_AUTO_STATUS) || '{"enabled": false, "lastRun": 0, "queue": []}'); } function setQueueData(data) { localStorage.setItem(KEY_AUTO_STATUS, JSON.stringify(data)); } function wait(ms) { return new Promise(r => setTimeout(r, ms)); } function logToWidget(msg, type = 'info') { let w = document.getElementById('mz-universal-widget'); if (!w) { w = document.createElement('div'); w.id = 'mz-universal-widget'; document.body.appendChild(w); } const time = new Date().toLocaleTimeString(); let spanClass = 'mz-log-info'; if (type === 'sent') spanClass = 'mz-log-sent'; if (type === 'success') spanClass = 'mz-log-success'; if (type === 'error') spanClass = 'mz-log-error'; if (type === 'warn') spanClass = 'mz-log-warn'; w.innerHTML = `<div class="mz-widget-row"><span class="mz-w-time">[${time}]</span><span class="${spanClass}">${msg}</span></div>` + w.innerHTML; w.style.display = 'block'; } // ================================================================================= // 3. CSS STİLLERİ // ================================================================================= if (!isWorkerMode) { const style = document.createElement('style'); style.innerHTML = ` #mz-main-panel { background-color: #2b2b2b; color: #fff; border: 1px solid #10559C; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5); width: 300px; float: right; clear: both; position: relative; margin-bottom: 15px; border-radius: 4px; font-family: Arial, sans-serif; z-index: 80; } #mz-panel-header { padding: 10px; background: linear-gradient(to bottom, #1E74B3, #1562A8); color: white !important; font-weight: bold; cursor: pointer; border: 2px solid white; box-sizing: border-box; display: flex; justify-content: space-between; align-items: center; user-select: none; font-size: 14px; border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); position: relative; z-index: 90; } #mz-panel-header:hover { background: linear-gradient(to bottom, #1562A8, #1E74B3); box-shadow: 0 1px 5px rgba(0, 0, 0, 0.6); } #mz-panel-body { padding: 10px; display: none; background-color: #1a1a1a; position: absolute; top: 100%; left: 0; width: 100%; box-sizing: border-box; border: 1px solid #444; border-top: none; box-shadow: 0 10px 20px rgba(0,0,0,0.9); z-index: 90; border-radius: 0 0 4px 4px; } #mz-panel-header span { color: white !important; text-shadow: 0 1px 1px rgba(0,0,0,0.4); } #mz-list-container { background: #2b2b2b !important; border: 1px solid #333 !important; min-height: 60px; max-height: 200px; overflow-y: auto; } #mz-universal-widget { position: fixed; bottom: 10px; right: 10px; width: 380px; height: auto; max-height: 250px; background: #000000; color: #00ff00; padding: 10px; border-radius: 4px; font-family: 'Courier New', Courier, monospace; font-size: 12px; font-weight: bold; z-index: 999999; border: 1px solid #00ff00; overflow-y: auto; display: none; box-shadow: 0 0 15px rgba(0, 255, 0, 0.2); pointer-events: none; transition: opacity 0.3s ease; line-height: 1.4; } .mz-widget-row { margin-bottom: 3px; text-shadow: 0 0 2px #003300; } .mz-w-time { color: #006400; margin-right: 8px; font-size: 11px; } .mz-log-sent { color: #00FFFF; font-weight: bold; text-shadow: 0 0 5px #00FFFF; } .mz-log-success { color: #00FF00; } .mz-log-error { color: #FF3333; } .mz-log-warn { color: #FFA500; } .mz-log-info { color: #CCCCCC; } .mz-user-row { padding: 2px 0; cursor: grab; user-select: none; } .mz-user-row:hover { background-color: #333; } .mz-user-row.dragging { opacity: 0.5; border: 1px dashed #ffcc00; background-color: #1a1a1a; } #mz-ayar-paneli { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; border: 1px solid #ccc; box-shadow: 0 5px 30px rgba(0,0,0,0.5); z-index: 10000; padding: 20px; border-radius: 8px; display: none; width: 800px; color: #333; font-family: Arial, sans-serif; } .ayar-satiri { display: flex; align-items: center; padding: 8px 4px; border-bottom: 1px solid #eee; } .ayar-baslik { font-weight: bold; background-color: #eee; display: flex; padding: 8px 4px; border-bottom: 2px solid #ccc; } .sutun { padding: 0 5px; } .sutun-teklif { flex: 1.5; } .sutun-mac { flex: 1.5; font-weight: bold; color: #007bff; } .sutun-aktif { flex: 0.5; text-align: center; } .sutun-taktik { flex: 3; } .buton-grup { margin-top: 15px; text-align: right; } .buton-grup button { padding: 8px 15px; margin-left: 5px; cursor: pointer; border: 1px solid #aaa; border-radius: 4px; } .kaydet-btn { background: #4CAF50; color: white; border: none !important; } .sifirla-btn { background: #FF9800; color: white; border: none !important; } .kapat-btn { background: #f44336; color: white; border: none !important; } `; document.head.appendChild(style); } // ================================================================================= // 4. VERİ YÖNETİMİ // ================================================================================= function getStoredUsers() { let raw = []; try { raw = JSON.parse(getPersistentData(KEY_USER_LIST, '[]')); } catch (e) { raw = []; } if (raw.length === 0) return []; let hasChanges = false; const normalized = raw.map(u => { if (typeof u === 'string') { hasChanges = true; return { name: u, days: [...VALID_DAYS] }; } else if (typeof u === 'object' && u !== null) { if (!Array.isArray(u.days)) { u.days = [...VALID_DAYS]; hasChanges = true; } return u; } return null; }).filter(u => u !== null); if (hasChanges) setPersistentData(KEY_USER_LIST, JSON.stringify(normalized)); return normalized; } function saveStoredUsers(usersObjArray, forceSort = false) { const userMap = new Map(); usersObjArray.forEach(user => { if (!userMap.has(user.name)) userMap.set(user.name, user); }); const uniqueList = Array.from(userMap.values()); if (forceSort) uniqueList.sort((a, b) => a.name.localeCompare(b.name)); setPersistentData(KEY_USER_LIST, JSON.stringify(uniqueList)); renderList(); } function getSelectedUsers() { return JSON.parse(getPersistentData(KEY_SELECTED_USERS, '[]')); } function saveSelectionState() { const checkboxes = document.querySelectorAll('.mz-user-checkbox'); const selected = Array.from(checkboxes).filter(cb => cb.checked).map(cb => cb.value); setPersistentData(KEY_SELECTED_USERS, JSON.stringify(selected)); } function getTacticsList() { return JSON.parse(getPersistentData(KEY_TACTIC_NAMES) || '[]').length > 0 ? JSON.parse(getPersistentData(KEY_TACTIC_NAMES)) : DEFAULT_TACTICS; } function fetchAndSaveTactics() { fetch(location.origin + '/?p=tactics').then(res => res.text()).then(html => { const doc = new DOMParser().parseFromString(html, 'text/html'); const newTactics = []; const letters = "abcdefghijklmnopqrst".split(""); letters.forEach(c => { const tabId = `tacticTab_${c}`; const spanId = `tactic${c.toUpperCase()}Tab`; const liElement = doc.getElementById(tabId); const spanElement = doc.getElementById(spanId); if (liElement && spanElement && !liElement.classList.contains('ui-state-disabled')) { newTactics.push({ val: c, txt: spanElement.innerText.trim() }); } }); if (newTactics.length > 0) setPersistentData(KEY_TACTIC_NAMES, JSON.stringify(newTactics)); }); } // ================================================================================= // 5. ARAYÜZ (PANEL) FONKSİYONLARI // ================================================================================= let dragSourceEl = null; let autoScrollInterval = null; const SCROLL_SPEED = 5; const SCROLL_AREA_SIZE = 20; function startAutoScroll(direction) { if (autoScrollInterval) return; const listContainer = document.getElementById('mz-list-container'); autoScrollInterval = setInterval(() => { if (direction === 'up') listContainer.scrollTop -= SCROLL_SPEED; else if (direction === 'down') listContainer.scrollTop += SCROLL_SPEED; }, 20); } function stopAutoScroll() { if (autoScrollInterval) { clearInterval(autoScrollInterval); autoScrollInterval = null; } } function handleDragStart(e) { dragSourceEl = this; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/html', this.innerHTML); this.classList.add('dragging'); } function handleDragEnd(e) { this.classList.remove('dragging'); stopAutoScroll(); saveSelectionState(); } function handleDragOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; const listContainer = document.getElementById('mz-list-container'); const rect = listContainer.getBoundingClientRect(); const mouseY = e.clientY; if (mouseY < rect.top + SCROLL_AREA_SIZE) startAutoScroll('up'); else if (mouseY > rect.bottom - SCROLL_AREA_SIZE) startAutoScroll('down'); else stopAutoScroll(); return false; } function handleDrop(e) { e.stopPropagation(); stopAutoScroll(); if (dragSourceEl !== this) { const listContainer = document.getElementById('mz-list-container'); const targetRow = this; const sourceRow = dragSourceEl; const children = Array.from(listContainer.children); const sourceIndex = children.indexOf(sourceRow); const targetIndex = children.indexOf(targetRow); if (sourceIndex < targetIndex) listContainer.insertBefore(sourceRow, targetRow.nextSibling); else listContainer.insertBefore(sourceRow, targetRow); } return false; } function renderList() { const c = document.getElementById('mz-list-container'); if (!c) return; c.innerHTML = ''; const allUsers = getStoredUsers(); if (allUsers.length === 0) { c.innerHTML = `<div style="padding:10px; color:#999; text-align:center; font-style:italic; font-size:11px;">${getText('emptyList')}</div>`; return; } const selectedUsers = getSelectedUsers(); allUsers.forEach((uObj, index) => { const uName = uObj.name; const validDayCount = uObj.days.filter(d => VALID_DAYS.includes(d)).length; const dayBadgeColor = validDayCount === 0 ? '#ff4444' : (validDayCount < 5 ? '#ffbb33' : '#00C851'); const d = document.createElement('div'); d.className = 'mz-user-row'; d.draggable = true; d.style.display = 'flex'; d.style.alignItems = 'center'; d.style.justifyContent = 'space-between'; d.style.padding = '4px'; d.addEventListener('dragstart', handleDragStart); d.addEventListener('dragover', handleDragOver); d.addEventListener('drop', handleDrop); d.addEventListener('dragend', handleDragEnd); const isChecked = selectedUsers.includes(uName) ? 'checked' : ''; d.innerHTML = ` <div style="display:flex; align-items:center; gap:5px; overflow:hidden;"> <input type="checkbox" class="mz-user-checkbox" value="${uName}" ${isChecked}> <span onclick="this.previousElementSibling.click()" style="cursor:pointer; font-weight:bold;">${uName}</span> </div> <button class="mz-day-config-btn" data-index="${index}" style="background:none; border:1px solid ${dayBadgeColor}; color:${dayBadgeColor}; border-radius:4px; cursor:pointer; font-size:9px; padding:2px 5px;" title="Günleri Düzenle"> 📅 ${validDayCount}/5 </button> `; c.appendChild(d); d.querySelector('.mz-user-checkbox').addEventListener('change', saveSelectionState); d.querySelector('.mz-day-config-btn').addEventListener('click', (e) => { e.stopPropagation(); openUserDaySettings(index); }); }); } function openUserDaySettings(userIndex) { const allUsers = getStoredUsers(); const userObj = allUsers[userIndex]; const dayNamesList = getText('dayNames'); const modalId = 'mz-user-day-modal'; document.getElementById(modalId)?.remove(); const modal = document.createElement('div'); modal.id = modalId; modal.style.cssText = 'position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:100000; display:flex; justify-content:center; align-items:center;'; let checksHtml = ''; VALID_DAYS.forEach(i => { const isChecked = userObj.days.includes(i) ? 'checked' : ''; checksHtml += `<label style="display:block; padding:8px 5px; cursor:pointer; border-bottom:1px solid #eee;"><input type="checkbox" class="mz-uday-check" value="${i}" ${isChecked}> ${dayNamesList[i]}</label>`; }); modal.innerHTML = ` <div style="background:#fff; padding:20px; border-radius:8px; width:260px; color:#333; font-family:Arial; box-shadow:0 0 20px rgba(0,0,0,0.5);"> <h4 style="margin-top:0; border-bottom:2px solid #007bff; padding-bottom:5px; color:#007bff;">${userObj.name}</h4> <div style="font-size:11px; color:#666; margin-bottom:10px;">${getText('modalHelper')}</div> <div style="margin-bottom:15px; max-height:250px; overflow-y:auto;">${checksHtml}</div> <div style="display:flex; justify-content:space-between; margin-top:10px;"> <button id="mz-uday-select-all" style="background:#607D8B; color:white; border:none; padding:5px 10px; cursor:pointer; border-radius:4px; font-size:11px;">${getText('btnSelectAll')}</button> <div> <button id="mz-uday-save" style="background:#4CAF50; color:white; border:none; padding:5px 15px; cursor:pointer; border-radius:4px;">${getText('btnSave')}</button> <button id="mz-uday-close" style="background:#f44336; color:white; border:none; padding:5px 10px; cursor:pointer; border-radius:4px; margin-left:5px;">${getText('btnCancel')}</button> </div> </div> </div>`; document.body.appendChild(modal); modal.querySelector('#mz-uday-close').onclick = () => modal.remove(); modal.querySelector('#mz-uday-select-all').onclick = () => { const allChecks = modal.querySelectorAll('.mz-uday-check'); const areAllChecked = Array.from(allChecks).every(cb => cb.checked); allChecks.forEach(cb => cb.checked = !areAllChecked); }; modal.querySelector('#mz-uday-save').onclick = () => { const newDays = []; modal.querySelectorAll('.mz-uday-check:checked').forEach(cb => newDays.push(parseInt(cb.value))); const freshList = getStoredUsers(); if (freshList[userIndex]) { freshList[userIndex].days = newDays; saveStoredUsers(freshList, false); } modal.remove(); }; } function initMainPanel() { const mainPanel = document.createElement('div'); mainPanel.id = 'mz-main-panel'; mainPanel.innerHTML = ` <div id="mz-panel-header"><span>${getText('panelTitle')}</span><span id="mz-arrow">▼</span></div> <div id="mz-panel-body"> <div style="margin-bottom:8px;"> <textarea id="mz-input-area" placeholder="${getText('inputPlaceholder')}" style="width:96%; height:40px; background:#333; color:#fff; border:1px solid #555; font-size:11px; margin-bottom:2px;"></textarea> <div style="display:flex; gap:2px;"> <button id="mz-add-btn" style="flex:1; background:#2196F3; color:white; border:none; padding:4px; cursor:pointer;">${getText('addBtn')}</button> <button id="mz-add-friends-btn" style="flex:1; background:#9C27B0; color:white; border:none; padding:4px; cursor:pointer;">${getText('addFriendsBtn')}</button> </div> </div> <div style="border-top:1px solid #444; border-bottom:1px solid #444; padding:5px 0; margin-bottom:8px;"> <div style="display:flex; justify-content:space-between; margin-bottom:3px;"> <span style="font-size:11px; color:#aaa;">${getText('listHeader')}</span> <div><a href="#" id="mz-select-all" style="color:#aaa;">${getText('fmSelectAll')}</a> / <a href="#" id="mz-delete-selected" style="color:#f44336;">${getText('fmDeleteSelected')}</a></div> </div> <div id="mz-list-container"></div> </div> <div style="background:#333; padding:5px; border-radius:4px; margin-bottom:8px; display:flex; align-items:center; justify-content:space-between;"> <label for="mz-auto-mode-toggle" style="cursor:pointer; color:#0f0; font-weight:bold; font-size:11px;">${getText('autoModeLabel')}</label> <input type="checkbox" id="mz-auto-mode-toggle" style="cursor:pointer;"> </div> <div style="display:flex; gap:2px; margin-bottom: 8px;"> <button id="mz-settings-toggle" style="flex:1; background:#607D8B; color:white; border:none; padding:6px; cursor:pointer;">${getText('settingsToggle')}</button> </div> <div style="display:flex; gap:5px;"> <button id="mz-start-btn" style="flex:1; background: #4CAF50; color: white; border: none; padding: 8px; cursor: pointer;">${getText('startBtn')}</button> <button id="mz-stop-btn" style="flex:1; background: #f44336; color: white; border: none; padding: 8px; cursor: pointer;">${getText('stopBtn')}</button> </div> </div> `; bindEvents(mainPanel); const targetID = 'back-to-overview'; const checkExist = setInterval(() => { const target = document.getElementById(targetID); if (target && target.parentNode && !document.getElementById('mz-main-panel')) { clearInterval(checkExist); target.parentNode.insertBefore(mainPanel, target); setTimeout(() => { fetchAndSaveTactics(); renderList(); const autoStatus = getQueueData(); const toggle = mainPanel.querySelector('#mz-auto-mode-toggle'); if (toggle) toggle.checked = autoStatus.enabled; }, 500); } }, 250); } function bindEvents(panel) { const header = panel.querySelector('#mz-panel-header'); const body = panel.querySelector('#mz-panel-body'); const arrow = panel.querySelector('#mz-arrow'); if (header) { header.onclick = function () { if (body.style.display === 'none' || body.style.display === '') { body.style.display = 'block'; arrow.innerText = '▲'; renderList(); } else { body.style.display = 'none'; arrow.innerText = '▼'; } }; } panel.querySelector('#mz-auto-mode-toggle').addEventListener('change', (e) => { const status = getQueueData(); status.enabled = e.target.checked; if (!e.target.checked) status.queue = []; setQueueData(status); logToWidget(e.target.checked ? getText('autoModeOn') : getText('autoModeOff'), e.target.checked ? 'success' : 'warn'); }); panel.querySelector('#mz-settings-toggle').addEventListener('click', () => { ayarlarPaneliniOlustur(); document.getElementById('mz-ayar-paneli').style.display = 'block'; }); panel.querySelector('#mz-add-btn').addEventListener('click', () => { const area = panel.querySelector('#mz-input-area'); const rawTxt = area.value.trim(); if (!rawTxt) return; const namesToAdd = rawTxt.split('\n').map(s => s.trim()).filter(s => s.length > 0); const currentStoredList = getStoredUsers(); namesToAdd.forEach(name => { const exists = currentStoredList.some(user => user.name === name); if (!exists) { currentStoredList.push({ name: name, days: [...VALID_DAYS] }); } }); saveStoredUsers(currentStoredList, true); area.value = ''; }); panel.querySelector('#mz-add-friends-btn').addEventListener('click', () => { const buddyList = document.getElementById('buddy_list'); if (!buddyList) { alert(getText('noBuddyList')); return; } const links = buddyList.querySelectorAll('a.clippable'); const namesToAdd = Array.from(links).map(l => l.innerText.trim()); const currentStoredList = getStoredUsers(); let addedCount = 0; namesToAdd.forEach(name => { const exists = currentStoredList.some(user => user.name === name); if (!exists) { currentStoredList.push({ name: name, days: [...VALID_DAYS] }); addedCount++; } }); saveStoredUsers(currentStoredList, true); alert(`${addedCount} yeni kişi eklendi.`); }); panel.querySelector('#mz-select-all').addEventListener('click', (e) => { e.preventDefault(); const cbs = document.querySelectorAll('.mz-user-checkbox'); const all = Array.from(cbs).every(cb => cb.checked); cbs.forEach(cb => cb.checked = !all); saveSelectionState(); }); panel.querySelector('#mz-delete-selected').addEventListener('click', (e) => { e.preventDefault(); const cbs = document.querySelectorAll('.mz-user-checkbox'); const toDeleteNames = Array.from(cbs).filter(cb => cb.checked).map(cb => cb.value); if (toDeleteNames.length > 0 && confirm(getText('confirmDelete', toDeleteNames.length))) { const currentList = getStoredUsers(); const newList = currentList.filter(u => !toDeleteNames.includes(u.name)); saveStoredUsers(newList, false); const currentSelected = getSelectedUsers(); const newSelected = currentSelected.filter(u => !toDeleteNames.includes(u)); setPersistentData(KEY_SELECTED_USERS, JSON.stringify(newSelected)); } }); panel.querySelector('#mz-start-btn').addEventListener('click', () => { const cbs = document.querySelectorAll('.mz-user-checkbox:checked'); const selectedNames = Array.from(cbs).map(cb => cb.value); if (selectedNames.length === 0) { alert(getText('noSelection')); return; } const allUsers = getStoredUsers(); const queueObjs = allUsers.filter(u => selectedNames.includes(u.name)); const status = getQueueData(); status.enabled = true; status.queue = queueObjs; status.lastRun = 0; // Manuel başlatıldığı için 0 yapıyoruz setQueueData(status); document.getElementById('mz-auto-mode-toggle').checked = true; location.reload(); }); panel.querySelector('#mz-stop-btn').addEventListener('click', () => { const status = getQueueData(); status.enabled = false; status.queue = []; setQueueData(status); location.reload(); }); } function ayarlarPaneliniOlustur() { if (document.getElementById('mz-ayar-paneli')) { document.getElementById('mz-ayar-paneli').style.display = 'block'; return; } const panel = document.createElement('div'); panel.id = 'mz-ayar-paneli'; let html = `<h3 style="border-bottom: 2px solid #ccc; padding-bottom:10px;">${getText('settingsTitle')}</h3> <div class="ayar-tablosu"> <div class="ayar-baslik"> <span class="sutun sutun-teklif">${getText('colOfferDay')}</span> <span class="sutun sutun-mac">${getText('colMatchDay')}</span> <span class="sutun sutun-aktif" style="text-align:center">${getText('colActive')}</span> <span class="sutun sutun-taktik">${getText('colTacticHome')}</span> <span class="sutun sutun-taktik">${getText('colTacticAway')}</span> </div>`; const currentTactics = getTacticsList(); const options = currentTactics.map(t => `<option value="${t.val}">${t.txt} (${t.val.toUpperCase()})</option>`).join(''); const customGroups = [ { matchDay: GUNLER[2], offerIndices: [1] }, { matchDay: GUNLER[4], offerIndices: [2, 3] }, { matchDay: GUNLER[5], offerIndices: [4] }, { matchDay: GUNLER[6], offerIndices: [5] }, { matchDay: GUNLER[1], offerIndices: [6, 0] } ]; const getOfferDayNames = (indices) => indices.map(i => GUNLER[i]).join(', '); customGroups.forEach(group => { const offerDays = getOfferDayNames(group.offerIndices); html += `<div class="ayar-satiri" data-indices="${group.offerIndices.join(',')}"> <span class="sutun sutun-teklif" style="font-size:12px;">${offerDays}</span> <span class="sutun sutun-mac" style="font-size:12px;">${group.matchDay}</span> <span class="sutun sutun-aktif"><input type="checkbox" class="gun-aktif-check"></span> <span class="sutun sutun-taktik"><select class="taktik-secim-home" style="display:none; width:95%; font-size:11px;">${options}</select></span> <span class="sutun sutun-taktik"><select class="taktik-secim-away" style="display:none; width:95%; font-size:11px;">${options}</select></span> </div>`; }); html += `</div> <div class="buton-grup"> <button class="kaydet-btn" style="background: #4CAF50; color: white;">${getText('saveButton')}</button> <button class="sifirla-btn" style="background: #FF5722; color: white;">${getText('resetButton')}</button> <button class="kapat-btn" style="background: #607D8B; color: white;">${getText('closeButton')}</button> </div> <div style="font-size:10px; color:#666; margin-top:10px; text-align:center;">${getText('settingsNote')}</div>`; panel.innerHTML = html; document.body.appendChild(panel); panel.querySelector('.kapat-btn').addEventListener('click', () => panel.style.display = 'none'); panel.querySelector('.kaydet-btn').addEventListener('click', () => { try { const settings = {}; panel.querySelectorAll('.ayar-satiri').forEach(row => { const active = row.querySelector('.gun-aktif-check').checked; const tacticHomeVal = row.querySelector('.taktik-secim-home').value; const tacticAwayVal = row.querySelector('.taktik-secim-away').value; row.dataset.indices.split(',').forEach(dayIndex => { settings[dayIndex] = { aktif: active, taktikHome: tacticHomeVal, taktikAway: tacticAwayVal }; }); }); setPersistentData(KEY_SETTINGS, JSON.stringify(settings)); alert(getText('alertSaved')); panel.style.display = 'none'; } catch (e) { alert(getText('alertSaveError') + e.message); } }); panel.querySelector('.sifirla-btn').addEventListener('click', () => { if (confirm(getText('alertResetConfirm'))) { removePersistentData(KEY_SETTINGS); removePersistentData(KEY_TACTIC_NAMES); location.reload(); } }); panel.querySelectorAll('.gun-aktif-check').forEach(cb => { cb.addEventListener('change', e => { const row = e.target.closest('.ayar-satiri'); row.querySelector('.taktik-secim-home').style.display = e.target.checked ? 'inline-block' : 'none'; row.querySelector('.taktik-secim-away').style.display = e.target.checked ? 'inline-block' : 'none'; }); }); loadSettingsToPanel(); } function loadSettingsToPanel() { const settings = JSON.parse(getPersistentData(KEY_SETTINGS, '{}')); document.querySelectorAll('#mz-ayar-paneli .ayar-satiri').forEach(row => { const idx = row.dataset.indices.split(',')[0]; const conf = settings[idx]; const cb = row.querySelector('.gun-aktif-check'); const selHome = row.querySelector('.taktik-secim-home'); const selAway = row.querySelector('.taktik-secim-away'); if (conf) { cb.checked = conf.aktif; selHome.value = conf.taktikHome || 'a'; selAway.value = conf.taktikAway || 'a'; selHome.style.display = conf.aktif ? 'inline-block' : 'none'; selAway.style.display = conf.aktif ? 'inline-block' : 'none'; } else { cb.checked = false; selHome.style.display = 'none'; selAway.style.display = 'none'; } }); } // ================================================================================= // 6. YÖNETİCİ (BACKGROUND MANAGER) FONKSİYONLARI (GÜNCELLENDİ: 6 SAAT KONTROLÜ) // ================================================================================= function initBackgroundManager() { const autoStatus = getQueueData(); const now = Date.now(); const lastRun = parseInt(autoStatus.lastRun) || 0; // Timestamp olarak al const selectedUsers = JSON.parse(getPersistentData(KEY_SELECTED_USERS, '[]')); // --- ZAMAN KONTROLÜ (6 SAAT) --- // Eğer 6 saat geçtiyse (21600000 ms) ve listede kullanıcı varsa çalıştır if (selectedUsers.length > 0 && (now - lastRun > INTERVAL_MS)) { // Otomatik mod kapalı olsa bile zaman geldiyse zorla aç if (!autoStatus.enabled) { autoStatus.enabled = true; setQueueData(autoStatus); console.log("[MZ Otomasyon] 6 saat doldu. Otomatik başlatıldı."); } } if (!autoStatus.enabled) return; // Eğer süre dolmadıysa ve kuyruk boşsa dur if ((now - lastRun < INTERVAL_MS) && autoStatus.queue.length === 0) return; // Kuyruk boşsa ve zamanı geldiyse kuyruğu doldur if (!autoStatus.queue || autoStatus.queue.length === 0) { const allUserObjects = getStoredUsers(); const queueObjects = allUserObjects.filter(u => selectedUsers.includes(u.name)); if (queueObjects.length > 0) { autoStatus.queue = queueObjects; // lastRun'ı hemen güncelleme, işlem bitince güncelleyeceğiz setQueueData(autoStatus); logToWidget(getText('logSystemStart', queueObjects.length), 'success'); } else { return; } } if (autoStatus.queue.length > 0) { logToWidget(getText('logRobotCall'), 'warn'); createWorkerIframe(); } window.addEventListener('message', function (e) { if (e.data.type === 'MZ_WORKER_LOG') { logToWidget(e.data.msg, e.data.logType); } if (e.data.type === 'MZ_WORKER_READY') { const currentStatus = getQueueData(); if (currentStatus.queue && currentStatus.queue.length > 0) { const nextUser = currentStatus.queue[0]; const allSettings = JSON.parse(getPersistentData(KEY_SETTINGS, '{}')); const allTactics = getTacticsList(); const iframe = document.getElementById('mz_worker_frame'); if (iframe) { iframe.contentWindow.postMessage({ type: 'MZ_START_JOB', user: nextUser, settings: allSettings, tactics: allTactics }, '*'); } } } if (e.data.type === 'MZ_WORKER_USER_DONE') { const currentStatus = getQueueData(); const finishedUserObj = currentStatus.queue.shift(); const finishedUserName = finishedUserObj ? finishedUserObj.name : "Unknown"; setQueueData(currentStatus); logToWidget(getText('logProcessDone', finishedUserName, currentStatus.queue.length), 'success'); const frame = document.getElementById('mz_worker_frame'); if (currentStatus.queue.length > 0) { if (frame) frame.src = frame.src; } else { // --- İŞLEM TAMAMLANDI: ZAMANI KAYDET --- currentStatus.lastRun = Date.now(); // Şimdiki zamanı kaydet setQueueData(currentStatus); logToWidget(getText('logAllComplete'), 'success'); if (frame) frame.remove(); setTimeout(() => { document.getElementById('mz-universal-widget')?.remove(); localStorage.setItem('mz_redirect_to_sent_offers', 'true'); location.reload(); }, 2000); } } }); } function createWorkerIframe() { if (document.getElementById('mz_worker_frame')) return; const iframe = document.createElement('iframe'); iframe.id = 'mz_worker_frame'; iframe.name = 'mz_auto_worker_frame'; iframe.src = 'https://www.managerzone.com/?p=challenges&mz_mode=worker'; iframe.style.width = '1366px'; iframe.style.height = '768px'; iframe.style.position = 'fixed'; iframe.style.bottom = '0'; iframe.style.right = '0'; iframe.style.opacity = '0.01'; iframe.style.pointerEvents = 'none'; iframe.style.zIndex = '99999'; iframe.style.left = '-10000px'; document.body.appendChild(iframe); } // ================================================================================= // 7. İŞÇİ (WORKER) FONKSİYONLARI // ================================================================================= function waitForSearchBox(attempts = 0) { const searchInput = document.getElementById('search-string'); if (searchInput) { if (window.parent) window.parent.postMessage({ type: 'MZ_WORKER_READY' }, '*'); window.addEventListener('message', (e) => { if (e.data.type === 'MZ_START_JOB') { processUserLoop(e.data.user, e.data.settings, e.data.tactics); } }); } else { if (attempts < 40) setTimeout(() => waitForSearchBox(attempts + 1), 500); else { if (window.parent) window.parent.postMessage({ type: 'MZ_WORKER_LOG', msg: getText('logErrorTimeout'), logType: 'error' }, '*'); if (window.parent) window.parent.postMessage({ type: 'MZ_WORKER_USER_DONE' }, '*'); } } } async function processUserLoop(userObj, globalSettings, globalTactics) { if (typeof userObj === 'string') { userObj = { name: userObj, days: [0, 1, 2, 3, 4, 5, 6] }; } const user = userObj.name; const allowedDays = userObj.days; const searchInput = document.getElementById('search-string'); const searchBtn = document.getElementById('fss-submit'); if (!searchInput) { window.parent.postMessage({ type: 'MZ_WORKER_USER_DONE' }, '*'); return; } const currentFound = document.querySelector('#search-response-found a'); const isAlreadySearched = currentFound && document.getElementById('search-string').value === user; if (!isAlreadySearched) { window.parent.postMessage({ type: 'MZ_WORKER_LOG', msg: getText('logSearch', user), logType: 'info' }, '*'); searchInput.value = user; searchInput.dispatchEvent(new Event('input', { bubbles: true })); if (searchBtn.onclick) searchBtn.click(); else searchBtn.dispatchEvent(new MouseEvent('click', { view: window, bubbles: true, cancelable: true })); await wait(2500); } const teamLink = document.querySelector('#search-response-found a'); if (!teamLink) { window.parent.postMessage({ type: 'MZ_WORKER_LOG', msg: getText('logNotFound', user), logType: 'warn' }, '*'); window.parent.postMessage({ type: 'MZ_WORKER_USER_DONE' }, '*'); return; } const targetTeamName = teamLink.innerText.trim(); const buttons = document.querySelectorAll('.fss-challenge-button'); const gunIsimleri = ["Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"]; const processedDays = new Set(); let totalSentForThisUser = 0; const isChallengeAlreadySent = (targetTeamName, dayText) => { const outTable = document.querySelector('#matches_out tbody'); if (!outTable) return false; const rows = outTable.querySelectorAll('tr'); for (let row of rows) { const dayCell = row.querySelector('td:nth-child(2) span'); const rowDay = dayCell ? dayCell.innerText.trim() : ""; if (!rowDay.includes(dayText)) continue; const teamLink = row.querySelector('td:nth-child(1) a'); const rowTeamName = teamLink ? teamLink.innerText.trim() : ""; if (rowTeamName === targetTeamName) return true; } return false; }; const getSettingKeyFromMatchDay = (matchDayIndex) => { if (matchDayIndex === 2) return '1'; if (matchDayIndex === 4) return '2'; if (matchDayIndex === 5) return '4'; if (matchDayIndex === 6) return '5'; if (matchDayIndex === 1) return '6'; return null; }; const applyTactic = (selector, tacticVal) => { if (!tacticVal) return; const storedTactics = globalTactics || []; const tacticConfig = storedTactics.find(t => t.val === tacticVal); const targetName = tacticConfig ? tacticConfig.txt.trim() : null; const selectEl = $(selector); if (selectEl.length > 0) { let foundIndex = -1; let foundValue = null; selectEl.find('option').each(function (index) { if (targetName && $(this).text().trim().toLowerCase() === targetName.toLowerCase()) { foundIndex = index; foundValue = $(this).val(); return false; } }); if (foundIndex === -1) { const letterIndex = tacticVal.toLowerCase().charCodeAt(0) - 97; if (letterIndex < selectEl[0].options.length) { foundIndex = letterIndex; foundValue = selectEl[0].options[letterIndex].value; } } if (foundIndex !== -1 && foundValue) { selectEl[0].selectedIndex = foundIndex; selectEl[0].value = foundValue; selectEl.val(foundValue); selectEl[0].dispatchEvent(new Event('change', { bubbles: true })); } } }; // DÖNGÜ: Kullanıcı için tüm uygun günleri tara ve gönder while (true) { const buttons = document.querySelectorAll('.fss-challenge-button'); let targetBtn = null; let dayText = ""; let settingKeyToUse = null; let detectedMatchDayCode = ""; const savedSettings = globalSettings || {}; for (let btn of buttons) { if (btn.offsetParent !== null && (btn.innerText.includes("Maç Teklifleri") || btn.innerText.includes("Challenge"))) { const dayContainer = btn.closest('.fss-d'); if (dayContainer) { const header = dayContainer.querySelector('.fss-header'); const dayNum = header.innerText.replace(/\D/g, ''); const currentDayText = `Gün ${dayNum}`; // Zaten bu oturumda işlediysek veya outTable'da varsa geç if (processedDays.has(currentDayText)) continue; if (isChallengeAlreadySent(targetTeamName, currentDayText)) continue; const dateStrong = dayContainer.querySelector('.date-range strong'); if (dateStrong) { const rawText = dateStrong.innerText; const dateMatch = rawText.match(/(\d{1,2})[\/.-](\d{1,2})/); if (dateMatch) { const dayVal = parseInt(dateMatch[1], 10); const monthVal = parseInt(dateMatch[2], 10) - 1; const now = new Date(); let yearVal = now.getFullYear(); if (now.getMonth() === 11 && monthVal === 0) yearVal++; const matchDateObj = new Date(yearVal, monthVal, dayVal); const dayIndex = matchDateObj.getDay(); if (!allowedDays.includes(dayIndex)) continue; const dayName = gunIsimleri[dayIndex]; const foundKey = getSettingKeyFromMatchDay(dayIndex); if (foundKey && savedSettings[foundKey] && savedSettings[foundKey].aktif) { settingKeyToUse = foundKey; targetBtn = btn; dayText = currentDayText; detectedMatchDayCode = dayName; break; } } } } } } if (!targetBtn || !settingKeyToUse) { // Kullanıcı için başka teklif atılacak gün kalmadı if (totalSentForThisUser === 0) { window.parent.postMessage({ type: 'MZ_WORKER_LOG', msg: getText('logNoDay', user), logType: 'info' }, '*'); } window.parent.postMessage({ type: 'MZ_WORKER_USER_DONE' }, '*'); return; } // Günü işle const configToUse = savedSettings[settingKeyToUse]; targetBtn.click(); await wait(2200); if (configToUse.taktikHome) applyTactic('select[name="tactic_home"]', configToUse.taktikHome); if (configToUse.taktikAway) applyTactic('select[name="tactic_away"]', configToUse.taktikAway); await wait(1000); let sendBtn = null; document.querySelectorAll('.send-challenge-btn').forEach(b => { if (b.offsetParent !== null) sendBtn = b; }); if (sendBtn) { sendBtn.click(); window.parent.postMessage({ type: 'MZ_WORKER_LOG', msg: getText('logOfferSent', user, dayText + " (" + detectedMatchDayCode + ")"), logType: 'sent' }, '*'); processedDays.add(dayText); totalSentForThisUser++; await wait(3500); // Modalın kapanması ve işlemin kaydedilmesi için bekle } else { const closeBtn = document.querySelector('.fss-reset'); if (closeBtn) closeBtn.click(); processedDays.add(dayText); // Hatalı/Dolu olsa bile bu oturumda tekrar deneme await wait(1000); } } } // ================================================================================= // 8. ÇALIŞTIRMA MANTIĞI // ================================================================================= if (isWorkerMode) { if (document.readyState === "complete" || document.readyState === "interactive") { waitForSearchBox(); } else { window.addEventListener('load', () => waitForSearchBox()); } } else if (window.top === window.self) { // Normal sayfadaysak: // 1. Panel arayüzünü sadece challenges sayfasındaysak yükle if (isChallengePage) initMainPanel(); // 2. Arka plan yöneticisini HER SAYFADA yükle (Artık 6 saat kontrolünü burada yapıyor) if (document.readyState === "complete" || document.readyState === "interactive") { initBackgroundManager(); } else { window.addEventListener('load', initBackgroundManager); } } if (localStorage.getItem('mz_redirect_to_sent_offers') === 'true') { localStorage.removeItem('mz_redirect_to_sent_offers'); $(document).ready(function () { setTimeout(() => { const targetTab = $('a[href="#matches_out_wrapper"]'); if (targetTab.length > 0) { targetTab.click(); $('html, body').animate({ scrollTop: targetTab.offset().top - 150 }, 500); } }, 800); }); } } /**************************************************************************************** * * * BÖLÜM 17: LINK ENHANCEMENTS (Link İyileştirmeleri) - v5.0 (Başlık Eşleştirme) * * * ****************************************************************************************/ function initializeLinkEnhancements() { 'use strict'; const currentUrl = window.location.href; const $ = jQuery; // --- Alt Modül: Lig Tablosu Link Düzeltici (Değişiklik Yok) --- if (currentUrl.includes('?p=league')) { const fixLeagueLinks = () => { const rows = document.querySelectorAll('.nice_table tbody tr'); if (rows.length === 0) return; rows.forEach(row => { const weirdLink = row.querySelector('a[href*="?p=league"][href*="tid="]'); if (weirdLink) { try { const hrefParts = weirdLink.getAttribute('href').split('?')[1]; const urlParams = new URLSearchParams(hrefParts); const tid = urlParams.get('tid'); if (tid) { weirdLink.href = `/?p=team&tid=${tid}`; } } catch (e) { // Sessizce geç } } }); }; fixLeagueLinks(); setInterval(() => { const contentDiv = document.getElementById('contentDiv'); if (!contentDiv) return; const links = contentDiv.querySelectorAll('a[href*="sub=league&lid="]'); const currentHash = links.length + "_" + (links[0]?.href || ""); if (window._lastLeagueLinkHash === currentHash) return; window._lastLeagueLinkHash = currentHash; fixLeagueLinks(); }, 2000); } // --- Alt Modül: Maç Sayfası Lig Linki Ekleme (YENİ MANTIK) --- else if (currentUrl.includes('?p=match&sub=result&mid=')) { // Profilden belirli bir kategoriye (etikete) ait ligi bulan fonksiyon const findSpecificLeagueFromProfile = (htmlContent, matchCategoryText) => { const doc = new DOMParser().parseFromString(htmlContent, 'text/html'); const infoBlock = doc.querySelector('#infoAboutTeam'); if (!infoBlock) return null; const rows = infoBlock.querySelectorAll('dd'); // "U18 Ligi" gibi başlıkları temizleyip eşleştirmek için yardımcı const cleanText = (txt) => txt.replace(':', '').trim().toLowerCase(); const targetCategory = cleanText(matchCategoryText); for (let row of rows) { const labelSpan = row.querySelector('.teamExpText'); if (!labelSpan) continue; const labelText = cleanText(labelSpan.textContent); // EŞLEŞTİRME MANTIĞI: // Maç başlığındaki yazı (örn: "U18 Ligi") ile profil satırındaki yazı eşleşiyor mu? if (labelText === targetCategory) { const link = row.querySelector('a'); if (link) { return { name: link.textContent.trim(), // Örn: "Turkey - div2.3" url: link.href // Örn: /?p=league&type=u18&sid=... }; } } } return null; }; const enrichMatchHeader = async () => { const wrapper = document.querySelector('#match-info-wrapper'); if (!wrapper) return; // 1. ADIM: Maçın Kategorisini H1'den Al (Örn: "U18 Ligi", "Lig", "Resmi Kupa") const headerH1 = wrapper.querySelector('h1'); if (!headerH1) return; const matchCategoryText = headerH1.textContent.trim(); if (!matchCategoryText) return; const subHeader = wrapper.querySelector('h2'); // Eğer H2 içinde zaten "divX.X" gibi çalışan bir link varsa dokunmayalım (Opsiyonel, duruma göre kaldırılabilir) // Ancak MZ bazen yanlış link veriyor (örnekteki u23_world hatası gibi), o yüzden her durumda düzeltmeyi deneyebiliriz. // if (subHeader && subHeader.querySelector('a')) return; // Kullanıcının kendi profilini çek (Maçın kategorisine uygun lig linkini bulmak için) try { // Kullanıcının kendi profilini çek (Hangi ligde olduğunu bulmak için) const response = await fetch('/?p=team'); const html = await response.text(); // H1'deki kategoriye (matchCategoryText) karşılık gelen ligi profilden bul const foundLeague = findSpecificLeagueFromProfile(html, matchCategoryText); if (foundLeague) { // URL'den tid parametresini temizle (temiz lig linki olsun) let cleanUrl = foundLeague.url; /* Eğer URL tam path değilse başına domain ekle (Browser otomatik halleder ama garanti olsun) */ // H2 etiketi varsa içini değiştir, yoksa oluştur. if (subHeader) { subHeader.innerHTML = `<a href="${cleanUrl}">${foundLeague.name}</a>`; } else { const newH2 = document.createElement('h2'); newH2.style.margin = "0 0 10px 10px"; newH2.style.padding = "0"; newH2.style.fontSize = "12px"; newH2.innerHTML = `<a href="${cleanUrl}">${foundLeague.name}</a>`; headerH1.after(newH2); } console.log(`[LinkEnhancer] Lig linki düzeltildi: ${matchCategoryText} -> ${foundLeague.name}`); } else { console.log(`[LinkEnhancer] Bu kategori (${matchCategoryText}) için profilde karşılık bulunamadı.`); } } catch (err) { console.error("[LinkEnhancer] Veri çekme hatası:", err); } }; enrichMatchHeader(); } } /**************************************************************************************** * * * BÖLÜM EK: SATIŞ VERGİSİ HESAPLAYICI (EVRENSEL DİL DESTEĞİ v7) * * * ****************************************************************************************/ function initializeSellTaxCalculator() { 'use strict'; const $ = jQuery; console.log('[MZ TAX v7] Modül Başlatıldı (Universal Language Fix).'); function formatDate(date) { return date.toLocaleDateString(Core.i18n.lang === 'tr' ? 'tr-TR' : 'en-GB'); } /** * Oyuncunun yaşını bulur. (Dilden bağımsız - Yapısal Analiz) * MZ HTML yapısında yaş, oyuncu bilgi tablosundaki kalın (strong) yazılan * ve 10-50 arasında olan İLK sayıdır. */ function getPlayerAge() { let age = null; // Yaşı ayıklayan yardımcı fonksiyon const extractAgeFromContainer = (container) => { // Konteyner içindeki tüm kalın (strong) etiketlerini bul const strongTags = container.find('strong'); for (let i = 0; i < strongTags.length; i++) { // Sayısal değeri al const val = parseInt($(strongTags[i]).text().trim(), 10); // Kontrol: Sayı mı? Mantıklı bir yaş aralığında mı? (15-50 arası) // Değer (Value) veya Maaş (Wage) çok yüksek sayılar olduğu için karışmaz. if (!isNaN(val) && val >= 10 && val <= 60) { return val; // İlk bulduğu mantıklı sayıyı yaş olarak döndür } } return null; }; // 1. YÖNTEM: Tıklanan son oyuncu kutusundan (Kadro Listesi) if (window.mzLastClickedPlayerContainer) { age = extractAgeFromContainer(window.mzLastClickedPlayerContainer); } // 2. YÖNTEM: Tekil Oyuncu Sayfası (.dg_playerview_info) if (!age && $('.dg_playerview_info').length > 0) { age = extractAgeFromContainer($('.dg_playerview_info')); } // 3. YÖNTEM: Mobil Görünüm (.pp-1-1) if (!age && $('.pp-1-1').length > 0) { age = extractAgeFromContainer($('.pp-1-1')); } return age; } // Tıklamaları izleyip son tıklanan oyuncu konteynerini kaydet $(document).on('click', '.sell_player a', function () { window.mzLastClickedPlayerContainer = $(this).closest('.playerContainer'); }); function watchForPopup() { if ($('#mz-tax-calculator-panel').length > 0) return; const helpBtns = $('.help_button'); helpBtns.each(function () { const btn = $(this); if (!btn.is(':visible')) return; if (btn.data('tax-processed')) return; const onClick = this.getAttribute('onclick'); if (!onClick || onClick.indexOf('showHelpLayer') === -1) return; try { // showHelpLayer parametrelerini al const match = onClick.match(/showHelpLayer\(['"]([^'"]+)['"]/); if (match) { const decoded = decodeURIComponent(match[1].replace(/\+/g, ' ')); // --- DİLDEN BAĞIMSIZ KONTROL --- // 1. Tarih formatı var mı? (Transfer geçmişi) const hasDate = /\d{1,2}[-./]\d{1,2}[-./]\d{4}/.test(decoded); // 2. HTML Tablosu var mı? (Altyapı oyuncularında vergi tablosu çıkar) // "tablesorter" sınıfı tüm dillerde aynıdır. const hasTable = decoded.includes('tablesorter'); if (hasDate || hasTable) { console.log('[MZ TAX v7] Hedef bulundu, panel ekleniyor.'); injectPanel(btn, decoded); btn.data('tax-processed', true); return false; } } } catch (e) { console.error("[MZ TAX v7] Hata:", e); } }); } function injectPanel(helpBtn, decodedHtml) { const dateRegex = /(\d{1,2})[-./](\d{1,2})[-./](\d{4})/; const dateMatch = decodedHtml.match(dateRegex); const panel = $('<div id="mz-tax-calculator-panel"></div>').css({ 'background': '#f1f8e9', 'border': '1px solid #81c784', 'padding': '8px', 'margin-top': '8px', 'border-radius': '4px', 'font-size': '11px', 'color': '#333', 'line-height': '1.4', 'z-index': '99999', 'position': 'relative', 'display': 'block', 'box-shadow': '0 2px 5px rgba(0,0,0,0.1)' }); let content = `<div style="font-weight:bold; color:#2e7d32; border-bottom:1px solid #a5d6a7; margin-bottom:4px;">${getText('calcTitle')}</div>`; if (dateMatch) { // --- TRANSFERLİ OYUNCU --- const day = parseInt(dateMatch[1], 10); const month = parseInt(dateMatch[2], 10) - 1; const year = parseInt(dateMatch[3], 10); const transferDate = new Date(year, month, day); // --- DÜZELTME: Saat farkını sıfırla ve +1 gün ekle --- const now = new Date(); now.setHours(0, 0, 0, 0); // Saati gece yarısına çekiyoruz const diffTime = Math.abs(now - transferDate); // "Geçen Süre" gösterimi için bugünü dahil et (+1) const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)) + 1; // OYUN MANTIĞI: Oyuncu listelendiğinde en erken 1 gün sonra satılır. // Bu yüzden vergi oranı, (Bugün + 1) gün sonrasına göre hesaplanmalıdır. // Örn: Bugün 70. gün ise, satış 71. gün olur ve vergi %15'e düşer. const effectiveSaleDays = diffDays + 1; // --- DÜZELTME SONU --- // Menajerler için önemli olan "Hangi gün listeye koyabilirim?" sorusudur. // Satış 1 gün sonra olduğu için, listeleme tarihi 1 gün erkendir. // %50 için: 29. gün satış olması lazım -> 28. gün (27 gün sonrası) listelenebilir. const date50 = new Date(transferDate); date50.setDate(date50.getDate() + 27); // %15 için: 71. gün satış olması lazım -> 70. gün (69 gün sonrası) listelenebilir. const date15 = new Date(transferDate); date15.setDate(date15.getDate() + 69); let taxRateDisplay = "15%"; let taxColor = "green"; // Hesaplamada 'diffDays' yerine 'effectiveSaleDays' kullanıyoruz if (effectiveSaleDays <= 28) { taxRateDisplay = "95%"; taxColor = "#d32f2f"; } else if (effectiveSaleDays <= 70) { taxRateDisplay = "50%"; taxColor = "#f57c00"; } // İçerik oluşturulurken "Geçen Süre" olarak hala 'diffDays' (70 gün) gösteriyoruz content += ` <div style="display:flex; justify-content:space-between;"> <span>${getText('currentTax')}:</span> <strong style="color:${taxColor}; font-size:12px;">${taxRateDisplay}</strong> </div> <div style="display:flex; justify-content:space-between;"> <span>${getText('daysElapsed')}:</span> <strong>${diffDays} ${getText('days')}</strong> </div> <hr style="margin:4px 0; border:0; border-top:1px dashed #ccc;"> <div style="display:flex; justify-content:space-between; color:${effectiveSaleDays > 28 ? '#999' : '#333'}"> <span>${getText('taxDrop50')}:</span> <span>${effectiveSaleDays > 28 ? getText('passed') : formatDate(date50)}</span> </div> <div style="display:flex; justify-content:space-between; color:${effectiveSaleDays > 70 ? '#999' : '#333'}"> <span>${getText('taxDrop15')}:</span> <span>${effectiveSaleDays > 70 ? getText('passed') : formatDate(date15)}</span> </div> `; } else { // --- ALTYAPI OYUNCUSU (Dilden Bağımsız Yaş Tespiti) --- const age = getPlayerAge(); let taxRate = "15%"; let taxColor = "green"; let nextInfo = ""; if (age !== null) { if (age <= 19) { taxRate = "25%"; taxColor = "#d32f2f"; nextInfo = `<div style="margin-top:4px; color:#555;">${getText('taxDrop20')}: <strong>${getText('nextSeason')}</strong></div>`; } else if (age === 20) { taxRate = "20%"; taxColor = "#f57c00"; nextInfo = `<div style="margin-top:4px; color:#555;">${getText('taxDrop15_age')}: <strong>${getText('nextSeason')}</strong></div>`; } else { taxRate = "15%"; taxColor = "green"; nextInfo = `<div style="margin-top:4px; color:green; font-style:italic;">${getText('minTaxReached')}</div>`; } content += ` <div style="color:#1565c0; margin-bottom:2px;">${getText('originalPlayer')}</div> <div style="display:flex; justify-content:space-between; align-items:center;"> <span>${getText('age')}: <strong>${age}</strong></span> <span>${getText('currentTax')}: <strong style="color:${taxColor}; font-size:12px;">${taxRate}</strong></span> </div> <hr style="margin:4px 0; border:0; border-top:1px dashed #ccc;"> ${nextInfo} `; } else { // Yaş bulunamazsa content += `<div style="color:#1565c0;">${getText('originalPlayer')}</div><div style="font-style:italic; font-size:10px;">${getText('originalNote')}</div>`; } } panel.html(content); const container = helpBtn.closest('td'); if (container.length > 0) { container.append(panel); } else { helpBtn.parent().parent().append(panel); } } setInterval(watchForPopup, 1000); } /**************************************************************************************** * * * BÖLÜM 18: TRANSFER GÖZLEMCİ FİLTRESİ (V43 - FİLTRE KAYMASI DÜZELTİLDİ) * * * ****************************************************************************************/ function initializeTransferScoutFilterScript() { 'use strict'; const $ = jQuery; console.log('[MZ Scout Filter] Modül Başlatıldı (V43 - Fixed Indexing).'); const STORAGE_KEY_PRESETS = 'mz_tsf_presets_v1'; const skillMap = [ { key: 'speed', tr: 'Hız', en: 'Speed', labelKey: 'skillSpeed' }, { key: 'stamina', tr: 'Dayanıklılık', en: 'Stamina', labelKey: 'skillStamina' }, { key: 'play_intelligence', tr: 'Oyun zekası', en: 'Play intelligence', labelKey: 'skillIntelligence' }, { key: 'passing', tr: 'Paslaşma', en: 'Passing', labelKey: 'skillPassing' }, { key: 'shooting', tr: 'Şut çekme', en: 'Shooting', labelKey: 'skillShooting' }, { key: 'heading', tr: 'Kafa vuruşu', en: 'Heading', labelKey: 'skillHeading' }, { key: 'keeping', tr: 'Kalecilik', en: 'Keeping', labelKey: 'skillKeeping' }, { key: 'ball_control', tr: 'Top kontrolü', en: 'Ball control', labelKey: 'skillControl' }, { key: 'tackling', tr: 'Top çalma', en: 'Tackling', labelKey: 'skillTackling' }, { key: 'aerial_passing', tr: 'Orta yapma', en: 'Aerial passing', labelKey: 'skillCrossing' }, { key: 'set_plays', tr: 'Duran top', en: 'Set plays', labelKey: 'skillSetPlays' } ]; let isScanning = false; let stopRequested = false; let collectedPlayerData = []; let filteredPlayerData = []; let scanPageCount = 0; let lastFirstPlayerId = ""; let minStarToCollect = 0; let currentPage = 1; const itemsPerPage = 10; // Global debounce utility used. // CSS GM_addStyle(` #mz-scout-filter-btn { float: right; margin-right: 5px; cursor: pointer; background: #6f42c1; color: white; border: 1px solid #59359a; font-weight:bold; } .tsf-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); z-index: 10050; display: none; justify-content: center; align-items: center; } .tsf-modal { background: #fff; padding: 20px; border-radius: 8px; width: 420px; text-align: center; box-shadow: 0 0 25px rgba(0,0,0,0.5); font-family: sans-serif; } .tsf-btn-start { padding: 12px; font-size: 15px; font-weight: bold; color: white; background: #28a745; border: none; border-radius: 5px; cursor: pointer; width: 100%; margin-top: 15px; transition: 0.2s; } .tsf-btn-start:hover { background: #218838; transform: scale(1.02); } .tsf-input-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; background: #f8f9fa; padding: 10px; border-radius: 4px; border: 1px solid #dee2e6; } .tsf-input-limit { padding: 5px; width: 60px; text-align: center; font-weight: bold; border: 1px solid #ccc; border-radius: 4px; font-size: 16px; color: #d63031; } .tsf-info-text { font-size: 11px; color: #d63031; text-align: right; margin-bottom: 15px; font-style: italic; } #tsf-scan-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.92); z-index: 20000; display: none; flex-direction: column; justify-content: center; align-items: center; color: white; } #tsf-scan-status { font-size: 24px; font-weight: bold; margin-bottom: 10px; color: #4CAF50; } #tsf-scan-count { font-size: 18px; color: #ddd; margin-bottom: 20px; line-height: 1.5; text-align: center; } .tsf-btn-stop { padding: 8px 30px; background: #dc3545; color: white; border: 2px solid #fff; border-radius: 50px; font-weight: bold; cursor: pointer; font-size: 14px; transition: transform 0.2s; } .tsf-btn-stop:hover { transform: scale(1.1); } .tsf-loader { border: 5px solid #333; border-top: 5px solid #00d2ff; border-radius: 50%; width: 60px; height: 60px; animation: spin 0.8s linear infinite; margin-bottom: 20px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } #tsf-dashboard { background: #1e272e; color: white; padding: 15px; margin-bottom: 15px; border-radius: 8px; position: relative; z-index: 100; border-bottom: 4px solid #0fbcf9; box-shadow: 0 5px 15px rgba(0,0,0,0.3); } /* PRESET BAR STYLE */ .tsf-preset-row { display: flex; align-items: center; gap: 8px; padding-bottom: 10px; margin-bottom: 15px; border-bottom: 1px solid rgba(255,255,255,0.1); flex-wrap: wrap; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 5px; } .tsf-preset-label { font-weight: bold; color: #ff9ff3; font-size: 13px; } #tsf-preset-select { padding: 5px; border-radius: 4px; border: none; font-size: 12px; background: #333; color: white; } #tsf-preset-name { padding: 5px; border-radius: 4px; border: none; font-size: 12px; width: 120px; } .tsf-btn-p { padding: 5px 10px; border-radius: 4px; border: none; font-size: 11px; cursor: pointer; color: white; font-weight: bold; } .tsf-btn-save { background: #00b894; } .tsf-btn-load { background: #0984e3; } .tsf-btn-del { background: #d63031; } .tsf-filter-row { display: flex; flex-wrap: wrap; gap: 15px; justify-content: center; align-items: center; margin-bottom: 15px; } .tsf-filter-group { background: rgba(255,255,255,0.08); padding: 5px 10px; border-radius: 5px; display: flex; align-items: center; gap: 8px; border: 1px solid rgba(255,255,255,0.1); } .tsf-filter-title { font-weight: bold; font-size: 12px; color: #ffd32a; margin-right: 5px; } .tsf-lbl { font-size: 12px; cursor: pointer; user-select: none; display: flex; align-items: center; } .tsf-lbl input { margin-right: 4px; } .tsf-skill-matrix { display: none; flex-wrap: wrap; gap: 10px; justify-content: center; background: rgba(0,0,0,0.2); padding: 15px; border-radius: 8px; border: 1px solid #333; margin-bottom: 15px; } .tsf-skill-matrix.show-matrix { display: flex !important; } .tsf-skill-box { display: flex; flex-direction: column; align-items: center; background: #2f3640; border: 1px solid #555; padding: 8px; border-radius: 5px; width: 100px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); transition: transform 0.2s; } .tsf-skill-box:hover { transform: translateY(-2px); border-color: #777; } .tsf-skill-name { font-size: 11px; font-weight: bold; color: #fff; margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; text-align: center; border-bottom: 1px solid #444; padding-bottom: 3px; } .tsf-chk-row { display: flex; align-items: center; justify-content: space-between; gap: 5px; font-size: 10px; color: #ddd; width: 100%; margin-bottom: 2px; cursor: pointer; } .tsf-chk-row input { margin: 0; cursor: pointer; } #tsf-counter { font-size: 14px; font-weight: bold; color: #0fbcf9; margin-left: auto; display: flex; align-items: center; gap: 10px; justify-content: space-between; width: 100%; border-top: 1px solid #444; padding-top: 10px; } .tsf-toggle-filters-btn { background: #16a085; color: white; border: none; padding: 6px 15px; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: bold; transition: background 0.2s; } .tsf-toggle-filters-btn:hover { background: #1abc9c; } .tsf-pagination-controls button { background: #3c40c6; color: white; border: none; padding: 5px 12px; border-radius: 3px; cursor: pointer; font-size: 12px; font-weight: bold; } .tsf-pagination-controls button:disabled { background: #57606f; cursor: not-allowed; } .tsf-p-wrap { content-visibility: auto; contain-intrinsic-size: 150px; margin-bottom: 10px; } #mz-tsf-results-container { contain: content; } `); function createUI() { if ($('#mz-scout-filter-btn').length > 0) return; const targetArea = $('.buttons-wrapper #tds'); if (targetArea.length) { const btn = $(`<a href="#" id="mz-scout-filter-btn" class="mzbtn buttondiv button_account"> <span class="buttonClassMiddle"><span><i class="fa fa-rocket"></i> ${getText('tsfBtnTitle')}</span></span> <span class="buttonClassRight"> </span> </a>`); btn.on('click', (e) => { e.preventDefault(); openModal(); }); targetArea.before(btn); } const modalHTML = ` <div id="tsf-modal-overlay" class="tsf-modal-overlay"> <div class="tsf-modal"> <h3 style="color:#333; margin:0 0 15px 0;">${getText('tsfModalTitle')}</h3> <p style="font-size:12px; color:#666; margin-bottom:20px;">${getText('tsfStartMsg')}</p> <div class="tsf-input-row"> <label style="font-size:13px; color:#333; font-weight:bold;">${getText('tsfMinStarLabel')}</label> <input type="text" id="tsf-min-hp" class="tsf-input-limit" value="" placeholder="2,3,4"> </div> <div class="tsf-info-text">${getText('tsfStarInfo')}</div> <button id="tsf-start-scan" class="tsf-btn-start">${getText('tsfBtnStart')}</button> <div style="margin-top:10px; cursor:pointer; color:#999; font-size:11px; text-decoration:underline;" onclick="$('#tsf-modal-overlay').hide()">${getText('tsfBtnClose')}</div> </div> </div> <div id="tsf-scan-overlay"> <div class="tsf-loader"></div> <div id="tsf-scan-status">${getText('tsfScanningMsg')}</div> <div id="tsf-scan-count"></div> <button id="tsf-stop-scan" class="tsf-btn-stop">${getText('tsfBtnStop')}</button> </div>`; $('body').append(modalHTML); $('#tsf-start-scan').on('click', startScanningProcess); $('#tsf-stop-scan').on('click', () => { stopRequested = true; $('#tsf-scan-status').text(getText('tsfStoppedMsg')); }); $('#tsf-min-hp').on('input', function () { let val = $(this).val(); val = val.replace(/[^234]/g, ''); if (val.length > 1) val = val.slice(-1); $(this).val(val); }); } function openModal() { $('#tsf-modal-overlay').css('display', 'flex'); } function isStarActive(element) { if (element.style.color === 'rgb(0, 0, 0)') return false; if (element.classList.contains('fa-star-o')) return false; const style = window.getComputedStyle(element); const color = style.color; const opacity = style.opacity; if (opacity && parseFloat(opacity) < 0.5) return false; if (color && color.includes('rgb')) { const rgb = color.match(/\d+/g); if (rgb && rgb.length >= 3) { const r = parseInt(rgb[0]), g = parseInt(rgb[1]), b = parseInt(rgb[2]); const saturation = Math.max(r, g, b) - Math.min(r, g, b); if (saturation < 20) return false; return true; } } return true; } function countActiveStarsInContainer(wrapper, selector) { let count = 0; wrapper.find(selector).each(function () { if (isStarActive(this)) count++; }); return count; } // --- DÜZELTİLMİŞ ANALİZ FONKSİYONU (V43) --- function analyzeSkillPotentials(container, playerName) { const skillAnalysis = { hp: [], lp: [] }; // ManagerZone Standart Beceri Sıralaması (Bu sıra asla değişmez) const skillOrder = [ 'speed', 'stamina', 'play_intelligence', 'passing', 'shooting', 'heading', 'keeping', 'ball_control', 'tackling', 'aerial_passing', 'set_plays' ]; // DÜZELTME: Sadece "skill_name" içeren satırları seçiyoruz. // Bu, başlıkları veya boş satırları atlar ve indeksin kaymasını önler. const skillRows = container.find('.skill_name').closest('tr'); skillRows.each(function (index) { // Eğer beklenenden fazla satır varsa (nadiren olur), hata vermesin diye durdur. if (index >= skillOrder.length) return; const currentSkillKey = skillOrder[index]; const supElement = $(this).find('.sup'); if (supElement.length > 0) { const supVal = supElement.text().trim(); if (currentSkillKey) { if (supVal === '1') { // Yüksek Potansiyel skillAnalysis.hp.push(currentSkillKey); } else if (supVal === '2') { // Düşük Potansiyel skillAnalysis.lp.push(currentSkillKey); } } } }); return skillAnalysis; } function extractBasicInfo(container) { const name = container.find('a.player_link').text().trim(); const link = container.find('a.player_link').attr('href'); let age = 0; const ageMatch = container.text().match(/(?:Age|Yaş):\s*(\d+)/i); if (ageMatch) age = parseInt(ageMatch[1]); return { name, link, age }; } // --- TARAMA --- function startScanningProcess() { console.clear(); console.log("--- TRANSFER TARAMASI BAŞLADI (V43) ---"); isScanning = true; stopRequested = false; collectedPlayerData = []; scanPageCount = 0; lastFirstPlayerId = ""; let rawMinStar = $('#tsf-min-hp').val(); minStarToCollect = (rawMinStar === '') ? 0 : parseInt(rawMinStar); $('#tsf-modal-overlay').hide(); $('#tsf-scan-overlay').css('display', 'flex'); $('#tsf-scan-count').html(`${getText('tsfPageInfo')} 0`); processPage(); } function processPage() { if (!isScanning) return; if (stopRequested) { finishScanning(); return; } const playersOnPage = $('.playerContainer'); if (playersOnPage.length === 0) { finishScanning(); return; } const currentFirstId = playersOnPage.first().find('.player_id_span').text().trim(); if (currentFirstId === lastFirstPlayerId && scanPageCount > 0) { finishScanning(); return; } lastFirstPlayerId = currentFirstId; scanPageCount++; playersOnPage.each(function () { const container = $(this); const basics = extractBasicInfo(container); const reportRow = container.find('.scout_report_row'); let counts = { hp: 0, lp: 0, speed: 0, unscouted: false }; if (reportRow.length === 0) { counts.unscouted = true; } else { counts.hp = countActiveStarsInContainer(reportRow.find('.high-stars'), 'i.fa-star'); counts.lp = countActiveStarsInContainer(reportRow.find('.low-stars'), 'i.fa-star'); counts.speed = countActiveStarsInContainer(reportRow.find('.scout_report_stars').eq(2), 'i.fa-star'); } if (counts.unscouted || counts.hp < minStarToCollect) return; const potentials = analyzeSkillPotentials(container, basics.name); // --- HIZLI RENKLENDİRME (DÜZELTİLMİŞ) --- // Buradaki mantık da yukarıdaki analyzeSkillPotentials ile aynı olmalı. const colorSkillOrder = [ 'speed', 'stamina', 'play_intelligence', 'passing', 'shooting', 'heading', 'keeping', 'ball_control', 'tackling', 'aerial_passing', 'set_plays' ]; // Yine güvenli satır seçiciyi kullanıyoruz const skillRows = container.find('.skill_name').closest('tr'); skillRows.each(function (index) { if (index >= colorSkillOrder.length) return; const currentKey = colorSkillOrder[index]; const row = $(this); // Renklendirilecek hedefi bul (Yazı veya Span) let target = row.find('td:first span.skill_name span:first'); if (target.length === 0) target = row.find('td:first'); // Yedek seçim if (potentials.hp.includes(currentKey)) { // Yüksek Potansiyel Rengi target.addClass(`gm_scout_h gm_s${counts.hp} skill_name_colored`); target.closest('td').addClass(`gm_scout_h gm_s${counts.hp}`); } else if (potentials.lp.includes(currentKey)) { // Düşük Potansiyel Rengi target.addClass(`gm_s${counts.lp} skill_name_colored`); target.closest('td').addClass(`gm_s${counts.lp}`); } }); // ---------------------------------------- let html = container[0].outerHTML; collectedPlayerData.push({ html: html, hp: counts.hp, lp: counts.lp, speed: counts.speed, unscouted: counts.unscouted, potentials: potentials, basics: basics }); }); $('#tsf-scan-count').html(`${getText('tsfPageInfo')} ${scanPageCount} <br> ${getText('tsfFoundInfo')} ${collectedPlayerData.length}`); const nextButton = findNextButton(); if (nextButton && nextButton.length > 0) { nextButton[0].click(); let checks = 0; const checkInt = setInterval(() => { if (stopRequested) { clearInterval(checkInt); finishScanning(); return; } if (!isScanning) { clearInterval(checkInt); return; } const newFirst = $('.playerContainer').first().find('.player_id_span').text().trim(); if (newFirst && newFirst !== lastFirstPlayerId) { clearInterval(checkInt); processPage(); } if (++checks > 40) { clearInterval(checkInt); finishScanning(); } }, 100); } else { finishScanning(); } } function findNextButton() { const container = $('.transferSearchPages.top:visible'); if (container.length === 0) return null; let activeEl = container.find('.nav_select, strong').first(); if (activeEl.length === 0) { container.contents().each(function () { if (this.nodeName !== 'A') { const txt = $(this).text().trim(); if (txt.length > 0 && !isNaN(parseInt(txt))) { activeEl = $(this); return false; } } }); } if (activeEl.length === 0) return null; let targetBtn = null; const activeNode = activeEl[0]; const allLinks = container.find('a'); allLinks.each(function () { const linkNode = this; if (activeNode.compareDocumentPosition(linkNode) & 4) { targetBtn = $(linkNode); return false; } }); return targetBtn; } function finishScanning() { isScanning = false; $('#tsf-scan-status').text(getText('tsfFinishMsg')); $('#tsf-stop-scan').hide(); setTimeout(() => { $('#tsf-scan-overlay').hide(); collectedPlayerData.sort((a, b) => { if (b.hp !== a.hp) return b.hp - a.hp; if (b.lp !== a.lp) return b.lp - a.lp; return b.speed - a.speed; }); filteredPlayerData = [...collectedPlayerData]; renderDashboardAndList(); }, 800); } // --- SONUÇ GÖSTERİMİ & PRESET YÖNETİMİ --- function getPresets() { try { const raw = localStorage.getItem(STORAGE_KEY_PRESETS); return raw ? JSON.parse(raw) : {}; } catch (e) { return {}; } } function savePreset(name) { if (!name) { alert(getText('tsfAlertEnterName')); return; } const config = { hp: $('.tsf-live-filter[data-type="hp"]:checked').map(function () { return parseInt(this.value) }).get(), lp: $('.tsf-live-filter[data-type="lp"]:checked').map(function () { return parseInt(this.value) }).get(), speed: $('.tsf-live-filter[data-type="speed"]:checked').map(function () { return parseInt(this.value) }).get(), matrix: {} }; $('.tsf-mx-yp:checked').each(function () { const k = $(this).data('key'); if (!config.matrix[k]) config.matrix[k] = []; config.matrix[k].push('yp'); }); $('.tsf-mx-notyp:checked').each(function () { const k = $(this).data('key'); if (!config.matrix[k]) config.matrix[k] = []; config.matrix[k].push('notyp'); }); $('.tsf-mx-notdp:checked').each(function () { const k = $(this).data('key'); if (!config.matrix[k]) config.matrix[k] = []; config.matrix[k].push('notdp'); }); const presets = getPresets(); presets[name] = config; localStorage.setItem(STORAGE_KEY_PRESETS, JSON.stringify(presets)); updatePresetDropdown(name); alert(getText('tsfAlertPresetSaved')); } function loadPreset(name) { const presets = getPresets(); const config = presets[name]; if (!config) return; $('.tsf-live-filter, .tsf-mx-yp, .tsf-mx-notyp, .tsf-mx-notdp').prop('checked', false); if (config.hp) config.hp.forEach(v => $(`.tsf-live-filter[data-type="hp"][value="${v}"]`).prop('checked', true)); if (config.lp) config.lp.forEach(v => $(`.tsf-live-filter[data-type="lp"][value="${v}"]`).prop('checked', true)); if (config.speed) config.speed.forEach(v => $(`.tsf-live-filter[data-type="speed"][value="${v}"]`).prop('checked', true)); if (config.matrix) { for (const [key, types] of Object.entries(config.matrix)) { if (types.includes('yp')) $(`.tsf-mx-yp[data-key="${key}"]`).prop('checked', true); if (types.includes('notyp')) $(`.tsf-mx-notyp[data-key="${key}"]`).prop('checked', true); if (types.includes('notdp')) $(`.tsf-mx-notdp[data-key="${key}"]`).prop('checked', true); } } applyLiveFilters(); alert(getText('tsfAlertPresetLoaded')); } function deletePreset(name) { if (!name) return; const presets = getPresets(); delete presets[name]; localStorage.setItem(STORAGE_KEY_PRESETS, JSON.stringify(presets)); updatePresetDropdown(); alert(getText('tsfAlertPresetDeleted')); } function updatePresetDropdown(selectName = null) { const presets = getPresets(); const sel = $('#tsf-preset-select'); sel.empty(); sel.append(`<option value="">-- ${getText('option')} --</option>`); for (const name of Object.keys(presets)) { sel.append(`<option value="${name}">${name}</option>`); } if (selectName) sel.val(selectName); } function renderDashboardAndList() { $('#players_container').remove(); $('.transferSearchPages').remove(); const newContainer = $('<div id="mz-tsf-results-container" class="players_transfer_container" style="min-height: 500px;"></div>'); let skillMatrixHTML = ''; skillMap.forEach(s => { const skillName = getText(s.labelKey); skillMatrixHTML += ` <div class="tsf-skill-box"> <span class="tsf-skill-name" title="${skillName}">${skillName}</span> <label class="tsf-chk-row" style="color:#2ecc71;">${getText('tsfFilterYP')} <input type="checkbox" class="tsf-mx-yp" data-key="${s.key}"></label> <label class="tsf-chk-row" style="color:#f39c12;">${getText('tsfFilterNotYP')} <input type="checkbox" class="tsf-mx-notyp" data-key="${s.key}"></label> <label class="tsf-chk-row" style="color:#e74c3c;">${getText('tsfFilterNotDP')} <input type="checkbox" class="tsf-mx-notdp" data-key="${s.key}"></label> </div> `; }); const dashboardHTML = ` <div id="tsf-dashboard"> <div class="tsf-preset-row"> <span class="tsf-preset-label">${getText('tsfPresetLabel')}</span> <select id="tsf-preset-select"></select> <button id="tsf-btn-load" class="tsf-btn-p tsf-btn-load">${getText('tsfBtnLoadPreset')}</button> <div style="flex-grow:1;"></div> <input type="text" id="tsf-preset-name" placeholder="${getText('tsfPhPresetName')}"> <button id="tsf-btn-save" class="tsf-btn-p tsf-btn-save">${getText('tsfBtnSavePreset')}</button> <button id="tsf-btn-del" class="tsf-btn-p tsf-btn-del">${getText('tsfBtnDeletePreset')}</button> </div> <div class="tsf-filter-row"> <div class="tsf-filter-group"> <span class="tsf-filter-title">${getText('tsfLblHP')}</span> <label class="tsf-lbl"><input type="checkbox" class="tsf-live-filter" data-type="hp" value="4" checked>4★</label> <label class="tsf-lbl"><input type="checkbox" class="tsf-live-filter" data-type="hp" value="3" checked>3★</label> </div> <div class="tsf-filter-group"> <span class="tsf-filter-title">${getText('tsfLblLP')}</span> <label class="tsf-lbl"><input type="checkbox" class="tsf-live-filter" data-type="lp" value="2" checked>2★</label> <label class="tsf-lbl"><input type="checkbox" class="tsf-live-filter" data-type="lp" value="1" checked>1★</label> </div> <div class="tsf-filter-group"> <span class="tsf-filter-title">${getText('tsfLblSpeed')}</span> <label class="tsf-lbl"><input type="checkbox" class="tsf-live-filter" data-type="speed" value="4" checked>4★</label> <label class="tsf-lbl"><input type="checkbox" class="tsf-live-filter" data-type="speed" value="3" checked>3★</label> <label class="tsf-lbl"><input type="checkbox" class="tsf-live-filter" data-type="speed" value="2" checked>2★</label> <label class="tsf-lbl"><input type="checkbox" class="tsf-live-filter" data-type="speed" value="1" checked>1★</label> </div> <button class="tsf-toggle-filters-btn">${getText('tsfToggleFilters')}</button> </div> <div class="tsf-skill-matrix"> ${skillMatrixHTML} </div> <div id="tsf-counter"> <span id="tsf-status-text">${getText('tsfCountVisible')} ${filteredPlayerData.length} / ${collectedPlayerData.length}</span> <div class="tsf-pagination-controls"> <button id="tsf-prev-btn" disabled>${getText('tsfPrevPage')}</button> <span id="tsf-page-info" class="tsf-page-info">${getText('tsfPageOf')} 1</span> <button id="tsf-next-btn">${getText('tsfNextPage')}</button> </div> </div> </div> <div id="tsf-player-list"></div> <div id="tsf-bottom-pagination" style="display: flex; justify-content: flex-end; align-items: center; gap: 10px; margin-top: 15px; padding-top: 10px; border-top: 1px solid #444;"> <div class="tsf-pagination-controls"> <button id="tsf-prev-btn-bottom" class="mz-script-button" style="background: #3c40c6; padding: 5px 12px; color: white; border: none; border-radius: 3px; cursor: pointer;">${getText('tsfPrevPage')}</button> <span id="tsf-page-info-bottom" style="font-weight: bold; color: #0fbcf9;">${getText('tsfPageOf')} 1</span> <button id="tsf-next-btn-bottom" class="mz-script-button" style="background: #3c40c6; padding: 5px 12px; color: white; border: none; border-radius: 3px; cursor: pointer;">${getText('tsfNextPage')}</button> </div> </div>`; newContainer.append(dashboardHTML); $('.transfer_window').last().after(newContainer); $(window).scrollTop(0); currentPage = 1; renderCurrentPage(); updatePresetDropdown(); const debouncedFilter = debounce(applyLiveFilters, 300); $(document).on('change', '.tsf-live-filter, .tsf-mx-yp, .tsf-mx-notdp, .tsf-mx-notyp', debouncedFilter); $('#tsf-btn-save').on('click', () => savePreset($('#tsf-preset-name').val().trim())); $('#tsf-btn-load').on('click', () => loadPreset($('#tsf-preset-select').val())); $('#tsf-btn-del').on('click', () => deletePreset($('#tsf-preset-select').val())); $('#tsf-preset-select').on('change', function () { $('#tsf-preset-name').val(this.value); }); $('#tsf-prev-btn').on('click', () => { if (currentPage > 1) { currentPage--; renderCurrentPage(true); } }); $('#tsf-next-btn').on('click', () => { const maxPage = Math.ceil(filteredPlayerData.length / itemsPerPage); if (currentPage < maxPage) { currentPage++; renderCurrentPage(true); } }); // Alt Navigasyon Tıklama Olayları $('#tsf-prev-btn-bottom').on('click', () => { if (currentPage > 1) { currentPage--; renderCurrentPage(false); document.getElementById('tsf-dashboard').scrollIntoView({ behavior: 'smooth' }); } }); $('#tsf-next-btn-bottom').on('click', () => { const maxPage = Math.ceil(filteredPlayerData.length / itemsPerPage); if (currentPage < maxPage) { currentPage++; renderCurrentPage(false); document.getElementById('tsf-dashboard').scrollIntoView({ behavior: 'smooth' }); } }); $('.tsf-toggle-filters-btn').on('click', () => { $('.tsf-skill-matrix').toggleClass('show-matrix'); }); } function renderCurrentPage(scrollToTop = false) { const listDiv = $('#tsf-player-list'); const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = Math.min(startIndex + itemsPerPage, filteredPlayerData.length); const pageData = filteredPlayerData.slice(startIndex, endIndex); let htmlBuffer = ''; pageData.forEach(p => { htmlBuffer += `<div class="tsf-p-wrap">${p.html}</div>`; }); listDiv.html(htmlBuffer); // TSF yeni HTML enjekte ettikten sonra renklendirmeleri yeniden tetikle // (mz-done / data-mz-row-processed işaretleri sıfırlandığından tam yeniden işlem gerekir) setTimeout(() => { if (typeof window._mzColorBalls === 'function') window._mzColorBalls(); if (typeof window._mzColorText === 'function') window._mzColorText(); }, 200); const maxPage = Math.ceil(filteredPlayerData.length / itemsPerPage) || 1; // Üst ve Alt butonların durumlarını güncelle $('#tsf-prev-btn, #tsf-prev-btn-bottom').prop('disabled', currentPage === 1); $('#tsf-next-btn, #tsf-next-btn-bottom').prop('disabled', currentPage === maxPage); // Üst ve Alt sayfa yazılarını güncelle const pageText = `${getText('tsfPageOf')} ${currentPage} / ${maxPage}`; $('#tsf-page-info').text(pageText); $('#tsf-page-info-bottom').text(pageText); if (scrollToTop) { document.getElementById('tsf-dashboard')?.scrollIntoView({ behavior: 'smooth' }); } } function applyLiveFilters() { const hpVals = $('.tsf-live-filter[data-type="hp"]:checked').map(function () { return parseInt(this.value) }).get(); const lpVals = $('.tsf-live-filter[data-type="lp"]:checked').map(function () { return parseInt(this.value) }).get(); const speedVals = $('.tsf-live-filter[data-type="speed"]:checked').map(function () { return parseInt(this.value) }).get(); const mustBeHP = []; const mustNotBeHP = []; const mustNotBeDP = []; $('.tsf-mx-yp:checked').each(function () { mustBeHP.push($(this).data('key')); }); $('.tsf-mx-notyp:checked').each(function () { mustNotBeHP.push($(this).data('key')); }); $('.tsf-mx-notdp:checked').each(function () { mustNotBeDP.push($(this).data('key')); }); filteredPlayerData = collectedPlayerData.filter(p => { if (p.unscouted) return false; const hpMatch = hpVals.length === 0 || hpVals.includes(p.hp); const lpMatch = lpVals.length === 0 || lpVals.includes(p.lp); const speedMatch = speedVals.length === 0 || speedVals.includes(p.speed); if (!(hpMatch && lpMatch && speedMatch)) return false; for (let skillKey of mustBeHP) { if (!p.potentials.hp.includes(skillKey)) return false; } for (let skillKey of mustNotBeHP) { if (p.potentials.hp.includes(skillKey)) return false; } for (let skillKey of mustNotBeDP) { if (p.potentials.lp.includes(skillKey)) return false; } return true; }); currentPage = 1; $('#tsf-status-text').text(`${getText('tsfCountVisible')} ${filteredPlayerData.length} / ${collectedPlayerData.length}`); renderCurrentPage(false); } setTimeout(createUI, 1000); const observer = new MutationObserver(() => createUI()); const target = document.querySelector('.buttons-wrapper'); if (target) observer.observe(target, { childList: true, subtree: true }); } /**************************************************************************************** * * * BÖLÜM 19: TEKİL OYUNCU PROFİLİ RENKLENDİRME (YP/DP - Sadece İsimler) * * * ****************************************************************************************/ function initializeSinglePlayerScoutColoring() { 'use strict'; const $ = jQuery; // Sadece tekil oyuncu sayfasında çalış (pid parametresi varsa) const params = new URLSearchParams(window.location.search); if (!params.has('pid')) return; const pid = params.get('pid'); // Sayfa dilini tespit et ve dile göre ayrı önbelleğ kullan const pageLang = document.documentElement.lang || $('meta[name="language"]').attr('content') || 'en'; const CACHE_KEY = 'mz_single_profile_scout_v2_' + pageLang; // Renk Kuralları (Sadece Metin İçin) function getStarColor(stars) { if (stars === 1) return '#ff0000'; // Kırmızı if (stars === 2) return '#b8860b'; // Koyu Altın if (stars === 3) return '#0000ff'; // Mavi if (stars === 4) return '#ff00ff'; // Fuşya return null; } // Önbellek Fonksiyonları async function getCache() { return JSON.parse(await GM_getValue(CACHE_KEY, '{}')); } async function setCache(data) { await GM_setValue(CACHE_KEY, JSON.stringify(data)); } function parseScoutHtml(html) { const doc = new DOMParser().parseFromString(html, "text/html"); const result = { hp: { skills: [], stars: 0 }, lp: { skills: [], stars: 0 }, found: false }; const starSpans = doc.querySelectorAll("dd span.stars"); if (starSpans.length === 0) return result; const divs = doc.querySelectorAll("dd div.flex-grow-1"); const countStars = (span) => span ? span.querySelectorAll(".lit").length : 0; const extractSkills = (el) => Array.from(el.querySelectorAll(".blurred span")) .map(s => s.textContent.trim()) .filter(t => t.length > 1 && !t.includes("Trzxy")); if (divs.length >= 2) { result.hp.stars = countStars(starSpans[0]); result.hp.skills = extractSkills(divs[0]); result.lp.stars = countStars(starSpans[1]); result.lp.skills = extractSkills(divs[1]); result.found = true; } return result; } // Sadece İsimleri Boyama İşlemi function paintSkillNames(report) { if (!report || !report.found) return; // Mobilde ana container .playerContainer yerine div.player-skills vb. olabilir let container = $('.player_skills, .player-skills').closest('.playerContainer'); if (container.length === 0) container = $('.player-skills').closest('div'); if (container.length === 0) container = $('body'); // Yetenek isimlerini bulmak için hem masaüstü hem mobil class'larını hedefle let skillElements = container.find("table.player_skills td > span.clippable, .player-skills span.skill_name span, .player-skills .skill_name span, td > span.skill_name span:first-child"); if (skillElements.length === 0) { // Yedek: Eğer yukarıdakiler bulamazsa klasik tr > td:first yapısını tara skillElements = container.find('.player_skills tr td:first-child, .player-skills .skill-row .skill-name'); } skillElements.each(function () { const el = $(this); const skillName = el.text().trim(); if (!skillName || skillName.includes(':') || !isNaN(parseInt(skillName))) return; let color = null; if (report.hp.skills.includes(skillName)) { color = getStarColor(report.hp.stars); } else if (report.lp.skills.includes(skillName)) { color = getStarColor(report.lp.stars); } if (color) { el.css({ 'color': color, 'font-weight': 'bold' }); // FIX: Diğer modüllerin (Bölüm 9, 10, 13) bu rengi "tıkalı" sanmaması için sınıf ekliyoruz. const stars = report.hp.skills.includes(skillName) ? report.hp.stars : report.lp.stars; const scoutClass = `gm_s${stars}`; el.addClass(scoutClass + ' skill_name_colored'); // Mobilde bazen iç içe spanlar olduğu için en yakındaki sarmalayıcıyı da boya const $wrapper = el.closest('td, .skill_name, .skill-name'); $wrapper.css('color', color).addClass(scoutClass); } }); // --- YP/DP Başlık Metni Eklemesi --- let headerEl = container.find('h2.subheader, h2.clearfix').first(); if (headerEl.length > 0) { container.find('.mz-scout-text-info, .mz-scout-text-info-row').remove(); // Varsa temizle let textArr = []; const hpLabel = 'Y.P'; const lpLabel = 'D.P'; if (report.hp && report.hp.skills && report.hp.skills.length > 0) { let hpColor = getStarColor(report.hp.stars) || '#000'; textArr.push(`<span style="color:${hpColor};">${hpLabel} ${report.hp.stars}: ${report.hp.skills.join('-')}</span>`); } if (report.lp && report.lp.skills && report.lp.skills.length > 0) { let lpColor = getStarColor(report.lp.stars) || '#000'; textArr.push(`<span style="color:${lpColor};">${lpLabel} ${report.lp.stars}: ${report.lp.skills.join('-')}</span>`); } if (textArr.length > 0) { let infoRow = $(`<div class="mz-scout-text-info-row" style="background: #fcfcfc; border: 1px solid #ececec; border-left: 3px solid #ccc; padding: 3px 10px; margin-top: 5px; margin-bottom: 8px; margin-left: 0px; border-radius: 2px; display: inline-block; box-shadow: 0 1px 2px rgba(0,0,0,0.02); font-size:12px; font-weight:bold; letter-spacing:0.5px;">${textArr.join(' <span style="color:#ccc; font-weight:normal; margin: 0 4px;">|</span> ')}</div>`); headerEl.after(infoRow); } } } async function run() { const cache = await getCache(); // Eski dil sorununu çözmek için: Bu oyuncunun önbelleğini temizle ve taze çek if (cache[pid]) { delete cache[pid]; await setCache(cache); } // Butonu kontrol et ve Türkçe olarak çek if ($('.player_icon_placeholder.scout_report').length > 0) { const url = `/ajax.php?p=players&sub=scout_report&pid=${pid}&sport=soccer&lang=tr`; GM_xmlhttpRequest({ method: "GET", url: url, onload: async function (response) { if (response.status === 200) { const data = parseScoutHtml(response.responseText); if (data.found) { cache[pid] = data; await setCache(cache); paintSkillNames(data); } } } }); } } // Başlat (Sayfa yüklenince) setTimeout(run, 500); } /**************************************************************************************** * * * BÖLÜM 20: TAKİP LİSTESİ RENKLENDİRME - TAM (GÜNCELLENDİ) * * * ****************************************************************************************/ function initializeShortlistTextColoring() { 'use strict'; const $ = jQuery; if (window.location.search.indexOf('p=shortlist') === -1) return; // Sayfa dilini tespit et ve dile göre ayrı önbellek kullan const pageLang = document.documentElement.lang || $('meta[name="language"]').attr('content') || 'en'; // Eski v8 cache'i güvenli şekilde sıfırla try { GM_setValue('mz_scout_report_cache_v8', '{}'); } catch (e) { } const CACHE_KEY = 'mz_scout_report_cache_v10_' + pageLang; const MAXED_SKILLS_CACHE = {}; // Top renkleri için konstantlar const IMG_RED = "data:image/gif;base64,R0lGODlhDAAKAJEDAP////8AAMyZmf///yH5BAEAAAMALAAAAAAMAAoAAAIk3BQZYp0CAAptxvjMgojTEVwKpl0dCQrQJX3T+jpLNDXGlDUFADs="; const IMG_BLUE = "data:image/gif;base64,R0lGODlhDAAKAJEDAP///8zM/wAA/////yH5BAEAAAMALAAAAAAMAAoAAAIk3CIpYZ0BABJtxvjMgojTIVwKpl0dCQbQJX3T+jpLNDXGlDUFADs="; function getStarColor(stars) { if (stars === 1) return '#ff0000'; if (stars === 2) return '#b8860b'; if (stars === 3) return '#0000ff'; if (stars === 4) return '#ff00ff'; return null; } function generateBallHtml(count, imgSource) { if (!count || count < 0) return ''; let html = '<div class="mz-custom-skill-container" style="display:inline-flex; align-items:center; vertical-align:middle; white-space:nowrap; height: 12px;">'; for (let i = 0; i < count; i++) { html += `<img src="${imgSource}" style="vertical-align:middle; margin-right:1px; width:11px !important; height:10px !important;">`; } html += '</div>'; return html; } let cache = {}; let saveTimeout; async function loadCache() { try { const val = await GM_getValue(CACHE_KEY); if (val) cache = JSON.parse(val); } catch (e) { cache = {}; } } function saveCacheDebounced() { clearTimeout(saveTimeout); saveTimeout = setTimeout(async () => { await GM_setValue(CACHE_KEY, JSON.stringify(cache)); }, 2000); } function parseScoutHtml(html) { const doc = new DOMParser().parseFromString(html, "text/html"); const result = { hp: { skills: [], stars: 0 }, lp: { skills: [], stars: 0 }, unscouted: false }; const starSpans = doc.querySelectorAll("dd span.stars"); if (starSpans.length === 0) { result.unscouted = true; return result; } const divs = doc.querySelectorAll("dd div.flex-grow-1"); const countStars = (span) => span ? span.querySelectorAll(".lit").length : 0; const extractSkills = (el) => Array.from(el.querySelectorAll(".blurred span")).map(s => s.textContent.trim()).filter(t => t.length > 2 && !t.includes("Trzxy")); if (divs.length >= 2) { result.hp.stars = countStars(starSpans[0]); result.hp.skills = extractSkills(divs[0]); result.lp.stars = countStars(starSpans[1]); result.lp.skills = extractSkills(divs[1]); } return result; } // YENİ: Açık lightbox'tan tıkalı özellikleri parse et function parseMaxedSkillsFromOpenTrainingLightbox() { const lightbox = document.getElementById('lightbox_player_training_graphs'); if (!lightbox) return []; // Lightbox görünür mü kontrol et const isVisible = !!(lightbox.offsetWidth || lightbox.offsetHeight || lightbox.getClientRects().length); if (!isVisible) return []; const svg = lightbox.querySelector('svg'); if (!svg) return []; const maxedSkills = []; const redLineYs = []; // Kırmızı yatay çizgileri bul const paths = svg.querySelectorAll('path'); paths.forEach((p) => { const stroke = (p.getAttribute('stroke') || '').toLowerCase(); const style = (p.getAttribute('style') || '').toLowerCase(); const isRed = stroke === 'red' || stroke.includes('rgb(255') || stroke.includes('rgba(255') || stroke.includes('#ff0000') || stroke.includes('#f00') || style.includes('stroke:red') || style.includes('stroke: rgb(255') || style.includes('stroke: rgba(255') || style.includes('stroke:#ff0000') || style.includes('stroke:#f00'); if (!isRed) return; const d = p.getAttribute('d') || ''; const nums = d.match(/-?\d+(\.\d+)?/g); if (!nums || nums.length < 4) return; const y1 = parseFloat(nums[1]); const y2 = parseFloat(nums[3]); if (!Number.isFinite(y1) || !Number.isFinite(y2)) return; if (Math.abs(y1 - y2) > 0.5) return; redLineYs.push(y1); }); if (redLineYs.length === 0) return []; // Y-ekseni skill isimlerini bul ve Y ile eşleştir const yAxisLabels = svg.querySelectorAll('g.highcharts-axis-labels.highcharts-yaxis-labels text, .highcharts-yaxis-labels text, text'); yAxisLabels.forEach((t) => { const skillName = (t.textContent || '').trim(); if (!skillName) return; const lower = skillName.toLowerCase(); if (lower.includes('genel') || lower.includes('olay') || lower.includes('kayıt')) return; const yAttr = t.getAttribute('y'); if (!yAttr) return; const labelY = parseFloat(yAttr); if (!Number.isFinite(labelY)) return; const hit = redLineYs.some(lineY => Math.abs(lineY - labelY) < 2.5); if (hit && !maxedSkills.includes(skillName)) { maxedSkills.push(skillName); } }); return maxedSkills; } // YENİ: Antrenman grafiğinden tıkalı özellikleri tespit et async function getMaxedSkillsFromTrainingGraph(pid) { // Check session cache first if (MAXED_SKILLS_CACHE[pid] && Array.isArray(MAXED_SKILLS_CACHE[pid])) { return MAXED_SKILLS_CACHE[pid]; } // Check persistent cache const cacheKey = `mz_maxed_${pid}`; let cached = await GM_getValue(cacheKey); if (cached) { try { const parsed = JSON.parse(cached); // Valid for 7 days if (Date.now() - parsed.timestamp < 7 * 24 * 60 * 60 * 1000) { MAXED_SKILLS_CACHE[pid] = parsed.data; return parsed.data; } } catch (e) { } } // 2. Lightbox açık mı kontrol et ve parse et const fromLightbox = parseMaxedSkillsFromOpenTrainingLightbox(); if (fromLightbox.length > 0) { MAXED_SKILLS_CACHE[pid] = fromLightbox; // Save to persistent await GM_setValue(cacheKey, JSON.stringify({ timestamp: Date.now(), data: fromLightbox })); return fromLightbox; } // 3. Lightbox yoksa boş dön (AJAX cevabında SVG olmuyor) return []; } async function colorizeRow(container, pid, report) { const maxedSkills = await getMaxedSkillsFromTrainingGraph(pid); container.find('img.skill').each(function () { const img = $(this); if (img.hasClass('mz-skill-processed')) return; const parentRow = img.closest('tr'); let skillLabelEl = parentRow.find('.clippable, span.skill_name span:first-child').first(); let skillName = skillLabelEl.length ? skillLabelEl.text().trim() : parentRow.find('td:first').text().trim(); if (!skillName || skillName.includes(':') || !isNaN(parseInt(skillName))) return; let isMaxed = maxedSkills.includes(skillName); if (!isMaxed) { const firstTd = parentRow.find('td').first(); const compColor = window.getComputedStyle(firstTd[0]).color; const isScoutColored = firstTd.hasClass('mz-scout-colored'); const hasRedFont = firstTd.find('font[color="red"]').length > 0; if (!isScoutColored && (compColor === 'rgb(255, 0, 0)' || compColor === 'red' || hasRedFont)) { isMaxed = true; } } if (isMaxed) { parentRow.attr('data-mz-maxed', 'true'); const targetImg = IMG_RED; let val = 0; const alt = img.attr('alt') || ''; if (alt.includes(':')) val = parseInt(alt.split(':')[1]); else { const m = parentRow.text().match(/\((\d+)\)/); if (m) val = parseInt(m[1]); } if (val > 0) { img.hide().addClass('mz-skill-processed'); const newHtml = generateBallHtml(val, targetImg); img.after(newHtml); } } img.addClass('mz-skill-processed'); }); if (report && !report.unscouted) { const skillRows = container.find('.player_skills tr, .skill_name').closest('tr'); skillRows.each(function () { const row = $(this); let skillLabelEl = row.find('.clippable, span.skill_name span:first-child'); if (skillLabelEl.length === 0) skillLabelEl = row.find('td').first(); const text = skillLabelEl.text().trim(); if (!text || text.length < 3) return; let color = null; if (report.hp.skills.includes(text)) color = getStarColor(report.hp.stars); else if (report.lp.skills.includes(text)) color = getStarColor(report.lp.stars); if (color) { skillLabelEl.addClass('mz-scout-colored').css({ 'color': color, 'font-weight': 'bold' }); skillLabelEl.closest('td').addClass('mz-scout-colored').css('color', color); } }); // --- YP/DP Başlık Metni Eklemesi --- let headerEl = container.find('h2.subheader, h2.clearfix').first(); if (headerEl.length === 0) headerEl = container.find('h2').first(); if (headerEl.length > 0) { container.find('.mz-scout-text-info, .mz-scout-text-info-row').remove(); // Varsa temizle let textArr = []; const hpLabel = 'Y.P'; const lpLabel = 'D.P'; if (report.hp && report.hp.skills && report.hp.skills.length > 0) { let hpColor = getStarColor(report.hp.stars) || '#000'; textArr.push(`<span style="color:${hpColor};">${hpLabel} ${report.hp.stars}: ${report.hp.skills.join('-')}</span>`); } if (report.lp && report.lp.skills && report.lp.skills.length > 0) { let lpColor = getStarColor(report.lp.stars) || '#000'; textArr.push(`<span style="color:${lpColor};">${lpLabel} ${report.lp.stars}: ${report.lp.skills.join('-')}</span>`); } if (textArr.length > 0) { let infoRow = $(`<div class="mz-scout-text-info-row" style="background: #fcfcfc; border: 1px solid #ececec; border-left: 3px solid #ccc; padding: 3px 10px; margin-top: 5px; margin-bottom: 8px; margin-left: 0px; border-radius: 2px; display: inline-block; box-shadow: 0 1px 2px rgba(0,0,0,0.02); font-size:12px; font-weight:bold; letter-spacing:0.5px;">${textArr.join(' <span style="color:#ccc; font-weight:normal; margin: 0 4px;">|</span> ')}</div>`); headerEl.after(infoRow); } } } } // Scout raporu işleme async function processSingleShortlistPlayer(container) { if (container.data('mz-shortlist-processed')) return; // container.data('mz-shortlist-processed', true); // FIX: Set flag only on success let pid = container.find('.player_id_span').text().trim(); if (!pid) return; // 1. Get Scout Report let report = cache[pid]; if (!report) { const url = `/ajax.php?p=players&sub=scout_report&pid=${pid}&sport=soccer&lang=${pageLang}`; try { let response = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: resolve, onerror: reject }); }); report = parseScoutHtml(response.responseText); if (report.unscouted) { const profileUrl = `/index.php?p=players&pid=${pid}`; let fetchSuccess = false; const profileResponse = await new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: profileUrl, onload: (res) => resolve(res.status === 200 ? res : null), onerror: () => resolve(null), ontimeout: () => resolve(null) }); }); if (profileResponse) { fetchSuccess = true; const profileDoc = new DOMParser().parseFromString(profileResponse.responseText, "text/html"); const scoutRow = profileDoc.querySelector('.scout_report_row'); if (scoutRow) { const countStars = (selector) => scoutRow.querySelectorAll(`${selector} i.fa-star:not(.fa-star-o)`).length; const hpStars = countStars('.high-stars'); const lpStars = countStars('.low-stars'); const skillsTable = profileDoc.querySelector('.player_skills'); if (skillsTable && (hpStars > 0 || lpStars > 0)) { report.found = true; report.unscouted = false; report.hp.stars = hpStars; report.lp.stars = lpStars; skillsTable.querySelectorAll('td').forEach(td => { const text = td.textContent.trim(); let skillName = td.querySelector('span.clippable')?.textContent.trim(); if (!skillName) skillName = text.replace(/[¹²]/g, '').trim(); if (text.includes('¹') || td.querySelector('.sup')?.textContent === '1') { if (!report.hp.skills.includes(skillName)) report.hp.skills.push(skillName); } if (text.includes('²') || td.querySelector('.sup')?.textContent === '2') { if (!report.lp.skills.includes(skillName)) report.lp.skills.push(skillName); } }); } } } if (!fetchSuccess && report.unscouted) { // Retry later } else { cache[pid] = report; saveCacheDebounced(); } } else { cache[pid] = report; saveCacheDebounced(); } } catch (e) { console.error(`Scout fetch error for ${pid}`, e); } } // 2. Colorize await colorizeRow(container, pid, report); // 3. Training Percentage if (window.MzTrainingModule && window.MzTrainingModule.processPlayer) { window.MzTrainingModule.processPlayer(container); } container.data('mz-shortlist-processed', true); // FIX: Success } async function run() { await loadCache(); const queue = []; let isProcessingQueue = false; const processQueue = async () => { if (isProcessingQueue) return; isProcessingQueue = true; while (queue.length > 0) { const container = queue.shift(); // Double check overlap/removal if (container && container.length > 0) { try { await processSingleShortlistPlayer(container); } catch (e) { console.error('Queue processing error', e); } // Small delay to be nice to server await new Promise(r => setTimeout(r, 100)); } } isProcessingQueue = false; }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const container = $(entry.target); // Avoid duplicates in queue if (!container.data('mz-queued') && !container.data('mz-shortlist-processed')) { container.data('mz-queued', true); queue.push(container); } observer.unobserve(entry.target); } }); // Trigger queue processQueue(); }, { rootMargin: "200px" }); function observeRows() { const players = $('.playerContainer'); const currentHash = players.length + "_" + (players.first().find('.player_id_span').text() || ""); if (window._lastShortlistHash === currentHash) return; window._lastShortlistHash = currentHash; players.each(function () { const el = $(this); if (!el.data('mz-lazy-observed')) { observer.observe(this); el.data('mz-lazy-observed', true); } }); } observeRows(); // Dinamik yüklemeler için setInterval (JANK önleyici) setInterval(() => { const players = $('.playerContainer'); if (players.length === 0) return; const firstPid = players.first().find('.player_id_span').text().trim(); const currentHash = players.length + "_" + firstPid; if (window._lastShortlistHash === currentHash) return; window._lastShortlistHash = currentHash; observeRows(); }, 1500); const run = () => { observeRows(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', run); } else { run(); } // Antrenman grafiklerine tıklama olayını dinle $(document).on('click', '.training-graphs-icon, .fa-chart-line-up', function (e) { if ($(e.target).closest('.training_camp_player, .player_tc_package_information').length > 0) return; const clicked = $(this); let pid = null; const fromContainerPid = clicked.closest('.playerContainer').find('.player_id_span').text().trim(); if (fromContainerPid) pid = fromContainerPid; if (!pid) { const href = clicked.closest('a[href]').attr('href') || clicked.attr('href') || ''; const m = href.match(/pid=(\d+)/); if (m) pid = m[1]; } setTimeout(async () => { if (pid) { const parsed = parseMaxedSkillsFromOpenTrainingLightbox(); if (parsed.length > 0) { MAXED_SKILLS_CACHE[pid] = parsed; } } const container = $(`.playerContainer .player_id_span`).filter((_, el) => $(el).text().trim() === pid).closest('.playerContainer'); if (container.length) { container.data('mz-shortlist-processed', false); processSingleShortlistPlayer(container); } }, 1200); }); $(document).on('click', '.mz_powerboxTopUpperImage, .mz_powerboxClose', function () { // Popup kapandığında gerekirse tetikleme yapılabilir }); } } /**************************************************************************************** * * * BÖLÜM 21: TRANSFER SAYFASI RENKLENDİRME (FİNAL SÜRÜMÜ) * * * ****************************************************************************************/ function initializeTransferMarketTextColoring() { 'use strict'; const $ = jQuery; function getStarColor(stars) { if (stars === 1) return '#ff0000'; // Kırmızı if (stars === 2) return '#b8860b'; // Koyu Altın if (stars === 3) return '#0000ff'; // Mavi if (stars === 4) return '#ff00ff'; // Fuşya return null; } function processPlayer(container) { if (container.attr('data-mz-text-colored') === 'true') return; let hpStars = 0; let lpStars = 0; const scoutRow = container.find('.scout_report_row'); if (scoutRow.length > 0) { hpStars = scoutRow.find('.high-stars i.fa-star:not(.fa-star-o)').length; lpStars = scoutRow.find('.low-stars i.fa-star:not(.fa-star-o)').length; if (hpStars === 0 && lpStars === 0) return; } else if (container.find('.sup').length === 0 && !container.text().includes('¹') && !container.text().includes('²')) { // Eğer doğrudan scout satırı yoksa (ör: ID araması), sayfa içinde .sup var mı kontrol et, yoksa atla return; } const hpColor = (hpStars > 0) ? getStarColor(hpStars) : '#ff0000'; // Varsayılan Kırmızı const lpColor = (lpStars > 0) ? getStarColor(lpStars) : '#0000ff'; // Varsayılan Mavi container.find('td').each(function () { const cell = $(this); const text = cell.text(); const hasSupClass = cell.find('.sup').length > 0; const isYP = text.includes('¹') || (hasSupClass && cell.find('.sup').text().trim() === '1'); const isDP = text.includes('²') || (hasSupClass && cell.find('.sup').text().trim() === '2'); if (isYP && hpColor) { this.style.setProperty('color', hpColor, 'important'); this.style.setProperty('font-weight', 'bold', 'important'); cell.find('*').each(function () { this.style.setProperty('color', hpColor, 'important'); }); } else if (isDP && lpColor) { this.style.setProperty('color', lpColor, 'important'); this.style.setProperty('font-weight', 'bold', 'important'); cell.find('*').each(function () { this.style.setProperty('color', lpColor, 'important'); }); } }); container.attr('data-mz-text-colored', 'true'); } function run() { // Eğer ana gözlemci robotu aktifse, buradaki ekstra taramayı beklet if (window._mzRosterScoutActive) { setTimeout(run, 3000); return; } let players = $('.playerContainer'); if (players.length === 0) { // Eğer ana container yoksa (ID araması sonucu vb.), tabloyu bul ve kapsayıcıyı seç players = $('table.player_skills').closest('div'); if (players.length === 0) players = $('table.player_skills').parent(); } // PERFORMANS FIX: Oyuncu sayısı değişmediyse atla const firstPid = players.first().find('a[href*="pid="]').attr('href')?.match(/pid=(\d+)/)?.[1] || ""; const currentHash = players.length + "_" + firstPid; if (window._lastTransferHash === currentHash) return; window._lastTransferHash = currentHash; if (players.length > 0) { players.each(function () { processPlayer($(this)); }); } } setTimeout(run, 1000); setTimeout(run, 2500); // Global'e kaydet - TSF filtrelemesi sonrası çağırabilsin window._mzColorText = run; // Dinamik yüklemeler için setInterval (JANK önleyici) setInterval(run, 1500); } /**************************************************************************************** * * * BÖLÜM 22: ANTRENMAN SAYFASI POPUP RENKLENDİRME (TECRÜBE VE FORM HARİÇ) * * * ****************************************************************************************/ function initializeTrainingPopupScoutColoring() { 'use strict'; const $ = jQuery; const CACHE_KEY = 'mz_scout_report_cache_v8'; let popupCache = null; // --- TOP RENKLERİ VE YARDIMCILAR --- const IMG_RED = "data:image/gif;base64,R0lGODlhDAAKAJEDAP////8AAMyZmf///yH5BAEAAAMALAAAAAAMAAoAAAIk3BQZYp0CAAptxvjMgojTEVwKpl0dCQrQJX3T+jpLNDXGlDUFADs="; const IMG_BLUE = "data:image/gif;base64,R0lGODlhDAAKAJEDAP///8zM/wAA/////yH5BAEAAAMALAAAAAAMAAoAAAIk3CIpYZ0BABJtxvjMgojTIVwKpl0dCQbQJX3T+jpLNDXGlDUFADs="; function generateBallHtml(count, imgSource, extraIconsHtml) { if (!count || count < 0) return ''; let html = '<div class="mz-custom-skill-container" style="display:inline-flex; align-items:center; vertical-align:middle; white-space:nowrap; height: 12px;">'; for (let i = 0; i < count; i++) { html += `<img src="${imgSource}" style="vertical-align:middle; margin-right:1px; width:11px; height:10px;">`; } if (extraIconsHtml) { html += `<span class="mz-training-icon-wrapper" style="margin-left:0px; display:inline-flex; align-items:center;">${extraIconsHtml}</span>`; } html += '</div>'; return html; } function getStarColor(stars) { if (stars === 1) return '#ff0000'; if (stars === 2) return '#b8860b'; if (stars === 3) return '#0000ff'; if (stars === 4) return '#ff00ff'; return null; } function paintPopupSkills(container, report, pid) { // Bugün çalışılan yeteneği bul const selectedOption = container.find('select.training-attr-selector option:selected, select[name="training_field"] option:selected').last(); const todayTrainingSkill = selectedOption.text().trim(); // 1. Yetenek Toplarını Renklendir container.find('img.skill').each(function () { const img = $(this); const parentRow = img.closest('tr'); // Satırdaki yetenek adını bul let skillNameEl = parentRow.find('.clippable, span.skill_name span:first-child'); if (skillNameEl.length === 0) skillNameEl = parentRow.find('td').first(); const skillName = skillNameEl.text().trim(); // FİLTRE: Tecrübe ve Form toplarına dokunma if (/tecrübe|experience|experiencia|rutin/i.test(skillName) || /form|estado de forma/i.test(skillName)) return; if (img.next().hasClass('mz-custom-skill-container')) return; if (img.hasClass('mz-skill-processed')) return; let skillValue = 0; const altText = img.attr('alt') || ''; if (altText) { const parts = altText.split(':'); if (parts.length > 1) { skillValue = parseInt(parts[1].trim(), 10); } } if (!skillValue || isNaN(skillValue)) return; // TIKALILIK KARARI (v9: gm_s1 kontrolü eklendi) let isMaxed = parentRow.find('.maxed').length > 0; if (!isMaxed) { const skillTd = parentRow.find('td').first(); const color = skillTd.css('color').toLowerCase(); // FIX: Kırmızı renk scout tarafından boyanmışsa (gm_s1) tıkalı DEĞİLDİR. isMaxed = (color === 'rgb(255, 0, 0)' || color === 'red' || skillTd.find('font[color="red"]').length > 0) && !skillTd.hasClass('gm_s1') && !parentRow.hasClass('gm_s1'); } const targetImg = isMaxed ? IMG_RED : IMG_BLUE; let extraIconsHtml = ''; if (skillName === todayTrainingSkill) { extraIconsHtml = '<img src="data:image/gif;base64,R0lGODlhBgAKAJEDAJnMZpmZmQAAAP///yH5BAEAAAMALAAAAAAGAAoAAAIRXCRhApAMgoPtVXXS2Lz73xUAOw==" style="vertical-align:middle;">'; } img.hide().addClass('mz-skill-processed'); const newHtml = generateBallHtml(skillValue, targetImg, extraIconsHtml); img.after(newHtml); }); // 2. YP/DP (Metin) Renklendirmesi const skillRows = container.find('.player_skills tr, .skill_name').closest('tr'); skillRows.each(function () { const row = $(this); let skillLabelEl = row.find('.clippable, span.skill_name span:first-child'); if (skillLabelEl.length === 0) skillLabelEl = row.find('td').first(); const skillName = skillLabelEl.text().trim(); if (!skillName || /tecrübe|experience|rutin|form/i.test(skillName) || skillName.includes(':') || !isNaN(parseInt(skillName))) return; let color = null; if (report.hp && report.hp.skills.includes(skillName)) { color = getStarColor(report.hp.stars); } else if (report.lp && report.lp.skills.includes(skillName)) { color = getStarColor(report.lp.stars); } if (color) { skillLabelEl.css({ 'color': color, 'font-weight': 'bold' }); skillLabelEl.closest('td').css('color', color); // Rengi diğer modüllerden (Section 10, 13) gizlemek için class ekleme const stars = report.hp.skills.includes(skillName) ? report.hp.stars : report.lp.stars; skillLabelEl.addClass('gm_s' + stars); } }); // --- YP/DP Başlık Metni Eklemesi --- let headerEl = container.find('h2.subheader, h2.clearfix').first(); if (headerEl.length > 0) { container.find('.mz-scout-text-info, .mz-scout-text-info-row').remove(); // Varsa temizle let textArr = []; const hpLabel = 'Y.P'; const lpLabel = 'D.P'; if (report.hp && report.hp.skills && report.hp.skills.length > 0) { let hpColor = getStarColor(report.hp.stars) || '#000'; textArr.push(`<span style="color:${hpColor};">${hpLabel} ${report.hp.stars}: ${report.hp.skills.join('-')}</span>`); } if (report.lp && report.lp.skills && report.lp.skills.length > 0) { let lpColor = getStarColor(report.lp.stars) || '#000'; textArr.push(`<span style="color:${lpColor};">${lpLabel} ${report.lp.stars}: ${report.lp.skills.join('-')}</span>`); } if (textArr.length > 0) { let infoRow = $(`<div class="mz-scout-text-info-row" style="background: #fcfcfc; border: 1px solid #ececec; border-left: 3px solid #ccc; padding: 3px 10px; margin-top: 5px; margin-bottom: 8px; margin-left: 0px; border-radius: 2px; display: inline-block; box-shadow: 0 1px 2px rgba(0,0,0,0.02); font-size:12px; font-weight:bold; letter-spacing:0.5px;">${textArr.join(' <span style="color:#ccc; font-weight:normal; margin: 0 4px;">|</span> ')}</div>`); headerEl.after(infoRow); } } container.attr('data-mz-scout-applied', pid); } function parseScoutHtml(html) { const doc = new DOMParser().parseFromString(html, "text/html"); const result = { hp: { skills: [], stars: 0 }, lp: { skills: [], stars: 0 }, found: false }; const starSpans = doc.querySelectorAll("dd span.stars"); if (starSpans.length === 0) return result; const divs = doc.querySelectorAll("dd div.flex-grow-1"); const countStars = (span) => span ? span.querySelectorAll(".lit").length : 0; const extractSkills = (el) => Array.from(el.querySelectorAll(".blurred span")).map(s => s.textContent.trim()).filter(t => t.length > 2); if (divs.length >= 2) { result.hp.stars = countStars(starSpans[0]); result.hp.skills = extractSkills(divs[0]); result.lp.stars = countStars(starSpans[1]); result.lp.skills = extractSkills(divs[1]); result.found = true; } return result; } async function processPopup() { const powerbox = $('.mz_powerboxContent:visible'); if (powerbox.length === 0) return; const pidSpan = powerbox.find('.player_id_span'); if (pidSpan.length === 0) return; const pid = pidSpan.text().trim(); if (powerbox.attr('data-mz-scout-applied') === pid || powerbox.attr('data-mz-scout-applying') === pid) return; // Korumayı aktif et (Double trigger engeli) powerbox.attr('data-mz-scout-applying', pid); // Bellek içi cache kullanımı (GM_getValue her seferinde okunmaz) if (!popupCache) { popupCache = JSON.parse(await GM_getValue(CACHE_KEY, '{}')); } if (popupCache[pid]) { paintPopupSkills(powerbox, popupCache[pid], pid); powerbox.removeAttr('data-mz-scout-applying'); } else { if (powerbox.find('.scout_report').length > 0) { const url = `/ajax.php?p=players&sub=scout_report&pid=${pid}&sport=soccer`; GM_xmlhttpRequest({ method: "GET", url: url, onload: async function (response) { const data = parseScoutHtml(response.responseText); if (data.found) { popupCache[pid] = data; await GM_setValue(CACHE_KEY, JSON.stringify(popupCache)); paintPopupSkills(powerbox, data, pid); } else { powerbox.attr('data-mz-scout-applied', pid); } powerbox.removeAttr('data-mz-scout-applying'); } }); } else { powerbox.attr('data-mz-scout-applied', pid); powerbox.removeAttr('data-mz-scout-applying'); } } } // Dinamik yüklemeler için setInterval (JANK önleyici) setInterval(() => { if ($('.powerbox:visible').length > 0 && (window.location.href.includes('training_report') || window.location.href.includes('p=players') || window.location.href.includes('training_camp'))) { processPopup(); } }, 1000); } /**************************************************************************************** * * * BÖLÜM 23: ANTRENMAN YÜZDESİ OTOMATİK GÖSTERİMİ (Training Percentage) * * * ****************************************************************************************/ function initializeTrainingPercentageScript() { 'use strict'; const $ = jQuery; // Tooltip CSS $('<style>').prop('type', 'text/css').html(` .mz-training-tooltip { position: absolute; background: rgba(20, 20, 25, 0.95); color: #e0e0e0; padding: 12px; border-radius: 8px; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 12px; z-index: 10000; /* Konsolide edildi */ box-shadow: 0 4px 15px rgba(0,0,0,0.6); pointer-events: none; line-height: 1.5; border: 1px solid #444; min-width: 240px; backdrop-filter: blur(5px); } .mz-test-tooltip-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #444; padding-bottom: 8px; margin-bottom: 8px; font-weight: 600; color: #fff; } .mz-tooltip-val { font-size: 14px; color: #ffeb3b; } .mz-tooltip-val.negative { color: #ff5252; } .mz-tooltip-row { display: flex; justify-content: space-between; align-items: center; padding: 3px 0; } .mz-tooltip-label { color: #aaa; font-size: 11px; } .mz-badge { display: inline-block; padding: 1px 5px; border-radius: 3px; font-size: 10px; font-weight: bold; color: #111; min-width: 20px; text-align: center; margin-right: 4px; } .mz-badge-gray { background: #bdbdbd; } /* 1-3 */ .mz-badge-yellow { background: #ffca28; } /* 4-6 */ .mz-badge-green { background: #66bb6a; } /* 7-9 */ .mz-badge-blue { background: #29b6f6; } /* 10 */ .mz-badge-neg { background: #ef5350; color: white; } .mz-gain-bar { display: inline-block; width: 6px; height: 6px; background: #66bb6a; margin-right: 1px; border-radius: 1px; } .mz-gain-bar.neg { background: #ef5350; } .mz-section-title { margin-top: 10px; margin-bottom: 5px; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: #777; border-bottom: 1px solid #333; padding-bottom: 2px; } /* --- COMPACT MODE FOR SMALL SCREENS / OVERFLOW --- */ .mz-tooltip-compact .mz-tooltip-header { padding-bottom: 4px; margin-bottom: 4px; } .mz-tooltip-compact .mz-tooltip-row { padding: 0; line-height: 1.4; } .mz-tooltip-compact .mz-section-title { margin-top: 6px; margin-bottom: 3px; padding-bottom: 1px; } .mz-tooltip-compact .mz-tooltip-subheader { margin-bottom: 2px; } .mz-tooltip-compact .mz-badge { padding: 0 4px; font-size: 9px; } .mz-tooltip-compact .mz-gain-bar { height: 5px; width: 5px; } /* --- DYNAMIC COLUMN EXPANSION FOR HISTORY --- */ .mz-history-grid { display: flex; flex-flow: column nowrap; gap: 5px; } .mz-history-grid > div { width: 220px; } `).appendTo('head'); /**************************************************************************************** * * * MERKEZI OYUNCU VERİ CACHE SİSTEMİ (Player Data Cache with localStorage) * * * ****************************************************************************************/ const PlayerDataCache = { cache: new Map(), // pid -> { skillBalls, potential, maxedSkills, training, lastUpdated } CACHE_KEY: 'mz_player_data_cache_v8', // FIX: BUMPED TO V8 TO FORCE RESET CACHE_EXPIRY_DAYS: 7, // Initialize: Load from localStorage init() { this.load(); // v4.12: Hız yüzdesi yanlış satıra enjekte edilme hatası düzeltildi — önbelleği temizle. const forceResetKey = 'mz_force_reset_v4_12'; if (!localStorage.getItem(forceResetKey)) { console.log('[TrainingModule] v4.12 - Clearing old cache for skill row match fix...'); localStorage.removeItem(this.CACHE_KEY); localStorage.removeItem('mz_player_data_cache_v8'); localStorage.removeItem('mz_player_data_cache_v7'); localStorage.setItem(forceResetKey, 'true'); console.log('[PlayerDataCache] Force reset for v4.12 completed.'); } this.cleanExpired(); }, // Save player data to cache savePlayerData(pid, data) { const existing = this.cache.get(pid) || {}; const updated = { ...existing, ...data, lastUpdated: Date.now() }; this.cache.set(pid, updated); // Persist to localStorage (debounced) this.debouncedPersist(); }, // Get player data from cache getPlayerData(pid) { return this.cache.get(pid); }, // Check if player data exists in cache hasPlayerData(pid) { return this.cache.has(pid); }, // Persist cache to localStorage persist() { try { const cacheData = Array.from(this.cache.entries()); localStorage.setItem(this.CACHE_KEY, JSON.stringify(cacheData)); } catch (e) { console.warn('[PlayerDataCache] Failed to persist to localStorage:', e); } }, // Debounced persist (avoid excessive localStorage writes) debouncedPersist: (() => { let timeout; return function () { clearTimeout(timeout); timeout = setTimeout(() => this.persist(), 2000); }; })(), // Load cache from localStorage load() { try { const stored = localStorage.getItem(this.CACHE_KEY); if (stored) { const cacheData = JSON.parse(stored); this.cache = new Map(cacheData); } } catch (e) { console.warn('[PlayerDataCache] Failed to load from localStorage:', e); this.cache = new Map(); } }, // Clean expired entries (older than CACHE_EXPIRY_DAYS) cleanExpired() { const now = Date.now(); const expiryMs = this.CACHE_EXPIRY_DAYS * 24 * 60 * 60 * 1000; let cleaned = 0; for (const [pid, data] of this.cache.entries()) { if (data.lastUpdated && (now - data.lastUpdated > expiryMs)) { this.cache.delete(pid); cleaned++; } } if (cleaned > 0) { console.log(`[PlayerDataCache] Cleaned ${cleaned} expired entries`); this.persist(); } }, // Clear all cache (for manual reset) clear() { this.cache.clear(); localStorage.removeItem(this.CACHE_KEY); console.log('[PlayerDataCache] Cache cleared'); }, // Get cache statistics getStats() { return { totalPlayers: this.cache.size, cacheSize: new Blob([localStorage.getItem(this.CACHE_KEY) || '']).size, oldestEntry: Math.min(...Array.from(this.cache.values()).map(d => d.lastUpdated || Date.now())) }; } }; // Initialize cache on script load PlayerDataCache.init(); // Expose to console for debugging // Usage: // - window.PlayerDataCache.getStats() - View cache statistics // - window.PlayerDataCache.clear() - Clear all cached data // - window.PlayerDataCache.cache.size - Number of cached players if (typeof unsafeWindow !== 'undefined') { unsafeWindow.PlayerDataCache = PlayerDataCache; } else { window.PlayerDataCache = PlayerDataCache; } const TrainingModule = { coefficients: { t1: 0.645, t2: 0.55, t3: 0.7, t4: 0.85, t5: 0.96, t6: 1.111, t7: 1.3, t8: 1.6, t9: 2.02, t10: 2.40 }, cache: new Map(), // Helper to calculate sum with Van.mz coefficients getSum(s) { const coeffs = { t1: 0.645, t2: 0.55, t3: 0.7, t4: 0.85, t5: 0.96, t6: 1.111, t7: 1.3, t8: 1.6, t9: 2.02, t10: 2.4 }; let sum = 0; for (let i = 1; i <= 10; i++) { // van.mz formula: coefficient * level sum += (s[`t${i}`] || 0) * coeffs[`t${i}`] * i; } return parseFloat(sum.toFixed(1)); }, // Helper for bar colors getColorForLevel(lvl) { if (lvl >= 7) return '#29b6f6'; // Blue if (lvl >= 4) return '#66bb6a'; // Green if (lvl >= 2) return '#ffca28'; // Yellow return '#bdbdbd'; // Gray }, // Efficiency color logic (Red to Green) getEfficiencyColor(val) { let v = parseFloat(val); if (isNaN(v)) v = 0; v = Math.max(0, Math.min(10, v)); const hue = Math.floor((v / 10) * 120); // 0-120 (Red to Green) return `hsl(${hue}, 90%, 60%)`; }, // Her yetenek için ayrı training data çek (Van.mz mantığı) async getSkillTrainingData(pid, skillIndex) { const cacheKey = `${pid}_${skillIndex}`; if (this.cache.has(cacheKey)) return this.cache.get(cacheKey); try { // skill_id: 0=Speed, 1=Stamina, 2=Intelligence, ... 10=SetPlays // API için +2 eklemek gerekiyor (Van.mz'deki gibi) const skillId = skillIndex + 2; if (!pid || pid === "NaN") return null; const url = `https://www.managerzone.com/ajax.php?p=trainingGraph&sub=getJsonTrainingHistory&sport=soccer&player_id=${pid}&skill_id=${skillId}`; let response; let retries = 3; while (retries > 0) { try { response = await fetch(url); if (response.ok) break; } catch (err) { // Network error } retries--; if (retries > 0) { // console.warn(`[TrainingModule] Fetch failed for PID ${pid}, retrying... (${3 - retries})`); await new Promise(r => setTimeout(r, 500)); } } if (!response || !response.ok) { return null; } const text = await response.text(); // PERFORMANS (JANK) DÜZELTMESİ: Regex match(/.../s) çok büyük stringlerde // (örn. oturum düştüğünde dönen devasa HTML) ana thread'i bloke edebilir. // Bunun yerine indexOf ve parantez sayma kullanıyoruz. const varIndex = text.indexOf('var series ='); if (varIndex === -1) return null; const startIndex = text.indexOf('[', varIndex); if (startIndex === -1) return null; let openCount = 0; let endIndex = -1; for (let i = startIndex; i < text.length; i++) { if (text[i] === '[') openCount++; else if (text[i] === ']') openCount--; if (openCount === 0) { endIndex = i + 1; break; } } if (endIndex === -1) return null; const jsonStr = text.substring(startIndex, endIndex); const series = JSON.parse(jsonStr); const result = this.calculateSkillPercentage(series); this.cache.set(cacheKey, result); return result; } catch (e) { return null; } }, // Tek bir yetenek için yüzde hesapla (Van.mz mantığı - Exact Port) calculateSkillPercentage(series) { // 1. Van.mz Coefficients const coefficients = { t1: 0.645, t2: 0.55, t3: 0.7, t4: 0.85, t5: 0.96, t6: 1.111, t7: 1.3, t8: 1.6, t9: 2.02, t10: 2.4 }; // Helper to create a clean stat object const createStat = () => ({ t1: 0, t2: 0, t3: 0, t4: 0, t5: 0, t6: 0, t7: 0, t8: 0, t9: 0, t10: 0 }); // Use central getSum const getSum = (s) => this.getSum(s); // 2. Parse Series // Robust check for Maxed (Red Line) - Handle spaces in rgba const isMaxed = series.some(s => s.type === 'line' && s.color && s.color.replace(/\s/g, '') === 'rgba(255,0,0,0.7)' ); let allPoints = []; series.forEach(s => { if (s.data && s.data.length > 0) { s.data.forEach(p => { if ((p.marker && p.marker.symbol) || p.iconType) { allPoints.push(p); } }); } }); if (allPoints.length === 0) { return { percent: '0.0', details: createStat(), neg: createStat() }; } // Sort by time (x), then by Event priority (Bars BEFORE Ball Event on same day) const pointPriority = (p) => { // Safe access to symbol let s = ""; if (p.marker && p.marker.symbol) s = p.marker.symbol; else if (p.iconType) s = p.iconType; if (/ball|puck/i.test(s)) return 2; // Ball Gain/Loss: Priority 2 (Last) return 1; // Training Bars: Priority 1 (First) }; const points = allPoints.sort((a, b) => { if (a.x !== b.x) return a.x - b.x; // FIX: Ensure ball events are processed AFTER training bars on the SAME day // to correctly reset the segment for the NEXT ball. return pointPriority(a) - pointPriority(b); }); // 3. Process Data (Segment Logic) // We maintain a list of segments. New segment starts on Ball Gain. let segments = [{ stat: createStat(), neg: createStat(), startTime: 0 }]; let currentSegment = segments[0]; let lastResetTime = 0; let lastTrainedTime = 0; let lastTrainedLevel = null; // Regular expressions from Van.mz const regBall = /ball|puck|top/i; // Added 'top' for potential localized icons const regBarPos = /bar_pos_(\d+)/; const regBarNeg = /bar_neg_(\d+)/; // console.log(`[TrainingModule] Processing ${points.length} points.`); points.forEach((point) => { let symbol = ""; if (point.marker && point.marker.symbol) { symbol = point.marker.symbol; } else if (point.iconType) { symbol = point.iconType; } // console.log(`[TrainingModule] Point: x=${point.x}, symbol=${symbol}`); // --- 1. COUNT STATS FIRST (Credit to Current Segment) --- // Extract Level from Icon Name const posMatch = symbol.match(regBarPos); const negMatch = symbol.match(regBarNeg); // Strict Check: Training day > Reset day (Unless it's the reset event itself, handled by order) // With "Stats First" logic: // If point is "Training+Ball", we add stats to current (Old), then split. // If point is just "Training", we add stats to current. // The 'startTime' check is less critical if we rely on event order, but keep for safety against old points? // Actually, if we sort correctly, we process in order. if (point.x >= currentSegment.startTime) { if (posMatch) { const lvl = parseInt(posMatch[1]); if (lvl >= 1 && lvl <= 10) { currentSegment.stat[`t${lvl}`]++; if (point.x > lastTrainedTime) { lastTrainedTime = point.x; lastTrainedLevel = lvl; } else if (point.x === lastTrainedTime) { lastTrainedLevel = Math.max(lastTrainedLevel || 0, lvl); } } } else if (negMatch) { const lvl = parseInt(negMatch[1]); if (lvl >= 1 && lvl <= 10) { currentSegment.neg[`t${lvl}`]++; } } } // --- 2. CHECK FOR BALL GAIN/LOSS (Split Segment) --- // Reset AFTER adding stats, so the gains on the transition day count towards the OLD level. // Also handles cases like 'bar_pos_9_ball' which contains both. if (regBall.test(symbol)) { // console.log(`[TrainingModule] SPLIT: Ball/Puck detected at ${point.x} (${symbol})`); if (point.x > lastResetTime) { const newSeg = { stat: createStat(), neg: createStat(), startTime: point.x }; segments.push(newSeg); currentSegment = newSeg; lastResetTime = point.x; // Gerileme sonucu top düştüyse (_ball içeriyorsa), eski koda sadık kalıp bu drop olayını // asıl YENİ segmentin başlangıcı sayıyoruz ki ekranda - değer kalmaya devam etsin, 0'lanıp yok olmasın! if (negMatch) { const lvl = parseInt(negMatch[1]); if (lvl >= 1 && lvl <= 10) { currentSegment.neg[`t${lvl}`]++; } } } } }); // 4. Calculate Final Percentage // Use the LAST segment (current progress) const finalSegment = segments[segments.length - 1]; const posSum = getSum(finalSegment.stat); const negSum = getSum(finalSegment.neg); // van.mz goal is always 100 units const goalUnits = 100; let percentValue; let hideZero = false; if (negSum > 0) { percentValue = -negSum; } else if (isMaxed) { percentValue = 0; hideZero = true; // Tıkalıysa 0% yazdırma } else { percentValue = posSum; } // Cap at -99.9 to 99.9 percentValue = Math.max(-99.9, Math.min(99.9, percentValue)); // Format: 0 without decimal, others with .toFixed(1) let percentStr = percentValue === 0 ? '0' : percentValue.toFixed(1); if (hideZero) percentStr = ""; // Compute Efficiency for this skill let totalUnits = 0; let totalDays = 0; segments.forEach(seg => { totalUnits += getSum(seg.stat); for (let i = 1; i <= 10; i++) { totalDays += (seg.stat[`t${i}`] || 0); } }); const efficiency = (totalDays > 0) ? (totalUnits / totalDays) : 0; return { percent: percentStr, details: finalSegment.stat, neg: finalSegment.neg, segments: segments, efficiency: efficiency, lastTrainedTime: lastTrainedTime, lastTrainedLevel: lastTrainedLevel }; }, intersectionObserver: null, progress: { total: 0, current: 0, active: false }, initObserver() { if (this.intersectionObserver) return; // Observer yapılandırması: Daha sıkı lazy-loading (50px) const options = { root: null, // viewport rootMargin: '50px', threshold: 0.01 }; this.intersectionObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const $target = $(entry.target); // Try to restore from cache first (fast path) const restored = this.restoreFromCache($target); // If not in cache, scan the player (slow path) if (!restored) { this.processSinglePlayer($target); } else { // Even if restored from cache, we might want to update General Efficiency this.updateGeneralEfficiency(this.getPid($target), $target); } // Tekrar izlemeye gerek yok this.intersectionObserver.unobserve(entry.target); } }); }, options); }, pendingBackgroundPlayers: [], isBackgroundProcessing: false, async processVisiblePlayers() { // FEEDBACK LOOP KORUMASI: Arka planda zaten işlem yapılıyorsa yeni tarama başlatma. // Bu, DOM enjeksiyonlarının observer'ı tetikleyip döngü oluşturmasını engeller. if (this.isBackgroundProcessing) { // console.log('[TrainingModule] Tarama atlandı: Arka plan işlemi devam ediyor.'); return; } this.initObserver(); const $containers = []; // 1. Standart yetenek tablolu kutuları bul $('table.player_skills').each(function () { let wrapper = $(this).closest('.playerContainer'); if (wrapper.length === 0) wrapper = $(this).parent(); $containers.push(wrapper[0]); }); // 2. Transfer sayfasında ek tespitler (yeni arama sonuçları için) if (window.location.href.includes('p=transfer')) { // Liste ve ızgara görünümleri için farklı seçiciler $('.playerContainer, .player_container, table.player_skills').each(function () { $containers.push(this); }); } // Eşsiz konteynırları al const uniqueElements = $.uniqueSort ? $.uniqueSort($containers) : $.unique($containers); const $uniqueContainers = $(uniqueElements); const newBatch = []; let restoredCount = 0; $uniqueContainers.each((index, el) => { const $container = $(el); // Zaten işlenmiş mi? if ($container.find('.skill_exact2').length > 0) return; // Önbellekten yüklemeyi dene const restored = this.restoreFromCache($container); if (restored) { restoredCount++; } else { // Yeni oyuncu: Kuyruğa ekle ve görünürlük takibine al this.intersectionObserver.observe(el); newBatch.push($container); } }); if (newBatch.length > 0) { console.log(`[TrainingModule] Yeni tarama başladı: ${newBatch.length} oyuncu kuyruğa alındı.`); this.progress.active = true; this.progress.total = newBatch.length; this.progress.current = 0; // Atomik güncelleme: Kuyruğu tamamen değiştir this.pendingBackgroundPlayers = [...newBatch]; this.startBackgroundWorker(); } else { if (restoredCount > 0) { console.log(`[TrainingModule] ${restoredCount} oyuncu önbellekten hızlıca yüklendi.`); } // İşlenecek yeni oyuncu yoksa ilerleme durumunu temizle this.progress.active = false; } }, async startBackgroundWorker() { if (this.isBackgroundProcessing || this.pendingBackgroundPlayers.length === 0) return; this.isBackgroundProcessing = true; const worker = async () => { try { if (!this.isBackgroundProcessing || this.pendingBackgroundPlayers.length === 0) { this.isBackgroundProcessing = false; return; } const $container = this.pendingBackgroundPlayers.shift(); // Double check if already processed or removed from DOM if ($container && $container.length > 0 && document.contains($container[0])) { if ($container.find('.skill_exact2').length === 0) { await this.processSinglePlayer($container); } } } catch (e) { console.error('[TrainingModule] Worker iteration error:', e); } finally { if (this.isBackgroundProcessing) { setTimeout(worker, 1000); } } }; worker(); }, getPid($container) { let pid = $container.find('.player_id_span').first().text().trim(); // If container is just the table or a tight wrapper, look up a bit if (!pid) { pid = $container.closest('div, tr, tbody, table').parent().find('.player_id_span').first().text().trim(); } if (!pid) { const href = $container.find('a[href*="pid="]').attr('href') || $container.closest('div, tr, tbody, table').parent().find('a[href*="pid="]').attr('href'); if (href) { const match = href.match(/pid=(\d+)/); if (match) pid = match[1]; } } if (!pid && window.location.href.includes('p=players') && window.location.href.includes('pid=')) { const urlParams = new URLSearchParams(window.location.search); pid = urlParams.get('pid'); } return pid || null; }, async processSinglePlayer($container) { try { // 1. Find PID const pid = this.getPid($container); if (!pid) return; // 2. Bu kap içinde Yetenek Tablosunu bul (Transfer sayfası AJAX'ta tablo gecikebilir) let $skillTable = $container.is('table.player_skills') ? $container : $container.find('table.player_skills').first(); if ($skillTable.length === 0 && window.location.href.includes('p=transfer')) { // 1 saniyeye kadar bekle (5 x 200ms) for (let i = 0; i < 5; i++) { await new Promise(r => setTimeout(r, 200)); $skillTable = $container.is('table.player_skills') ? $container : $container.find('table.player_skills').first(); if ($skillTable.length > 0) break; } } let isListView = false; if ($skillTable.length === 0) { // YEDEK: Transfer/Arama listesi görünümü veya farklı class kullanan tablolar // 1. İçeride herhangi bir TABLO var mı? (Transfer Tekil Sayfası tespiti) if ($container.find('table').length > 0) { $skillTable = $container.find('table').first(); isListView = false; } else { // 2. Tablo yok, bu bir Liste/Satır görünümü olabilir (Yatay) $skillTable = $container; isListView = true; } } const skills = [ { key: 'skillSpeed', index: 0 }, { key: 'skillStamina', index: 1 }, { key: 'skillIntelligence', index: 2 }, { key: 'skillPassing', index: 3 }, { key: 'skillShooting', index: 4 }, { key: 'skillHeading', index: 5 }, { key: 'skillKeeping', index: 6 }, { key: 'skillControl', index: 7 }, { key: 'skillTackling', index: 8 }, { key: 'skillCrossing', index: 9 }, { key: 'skillSetPlays', index: 10 } ]; // Prioritized Parallel Processing with Throttling (Simulated) // We show % as they arrive, finish with General Efficiency let completedSkills = 0; const trainingData = {}; const isTransferPage = window.location.href.includes('p=transfer'); if (isTransferPage) { const processSkill = async (skill) => { let $skillRow; if (!isListView) { // DÜZELTME: Strict eşleşme — includes() çok gevşek. $skillRow = $skillTable.find('tr').filter(function () { const $row = $(this); const $clippable = $row.find('.clippable'); const $skillName = $row.find('.skill_name span:first-child'); const $firstTd = $row.find('td').first(); let rowSkillName = ''; if ($clippable.length > 0) rowSkillName = $clippable.text().trim(); else if ($skillName.length > 0) rowSkillName = $skillName.text().trim(); else rowSkillName = $firstTd.text().trim().replace(/[¹²\d\s%]+$/, '').trim(); rowSkillName = rowSkillName.replace(/\s+/g, ' ').trim(); const trName = Core.i18n.TRANSLATIONS['tr'][skill.key]; const enName = Core.i18n.TRANSLATIONS['en'][skill.key]; return rowSkillName === trName || rowSkillName === enName; }).first(); } else { $skillRow = $skillTable.find('td').filter(function () { const $img = $(this).find('img').first(); if ($img.length === 0) return false; const alt = $img.attr('alt') || ''; const trName = Core.i18n.TRANSLATIONS['tr'][skill.key]; const enName = Core.i18n.TRANSLATIONS['en'][skill.key]; return alt.startsWith(trName) || alt.startsWith(enName); }).first(); } if (!$skillRow || $skillRow.length === 0) return; // Skip if maxed or already injected if ($skillRow.find('.skill_exact2').length > 0) return; let isMaxed = false; if ($skillRow.attr('data-mz-maxed') === 'true') isMaxed = true; const $skillValueTd = $skillRow.find('td.skillval'); if (!isMaxed && $skillValueTd.find('.maxed').length > 0) isMaxed = true; if (!isMaxed) { const IMG_RED = "data:image/gif;base64,R0lGODlhDAAKAJEDAP////8AAMyZmf///yH5BAEAAAMALAAAAAAMAAoAAAIk3BQZYp0CAAptxvjMgojTEVwKpl0dCQrQJX3T+jpLNDXGlDUFADs="; if ($skillValueTd.find(`img[src="${IMG_RED}"]`).length > 0) isMaxed = true; } if (!isMaxed) { const $valSpan = $skillValueTd.find('span'); const color = $valSpan.css('color'); // FIX: Eğer kırmızı renk gm_s1 sınıfından geliyorsa (DP=1), bu tıkalı olduğu anlamına gelmez. if ((color === 'rgb(255, 0, 0)' || color === 'red' || $skillValueTd.find('font[color="red"]').length > 0) && !$skillRow.hasClass('gm_s1') && !$skillRow.find('.gm_s1').length) { isMaxed = true; } } // SADECE Hız (0) ve Dayanıklılık (1) için tıkalı olsa dahi gerileme araştırması yap (MZ Aging Deterioration), diğerlerinde atla if (isMaxed && skill.index > 1) return; const result = await this.getSkillTrainingData(pid, skill.index); if (result) { // 1. IMMEDIATELY INJECT PERCENTAGE this.injectPercentageToRow($skillRow, result.percent, pid, skill.index); trainingData[skill.index] = result; } }; for (const skill of skills) { await processSkill(skill); completedSkills++; // Kademeli gecikme (network throttle) - connection timeout'lara karsi await new Promise(r => setTimeout(r, 200)); } // All skills done for this player this.checkAllSkillsComplete(pid, $container, completedSkills, skills.length, trainingData); } else { // PERFORMANS DÜZELTMESİ: forEach(async) → for...of // forEach ile async kullanmak tüm skill'leri aynı anda paralel fetch eder. // 58 oyuncu × 11 skill = 638 eş zamanlı HTTP isteği tarayıcıyı kilitliyordu. // for...of ile sıralı işleme yapılır, ağ trafiği kontrollü kalır. for (const skill of skills) { let $skillRow; if (!isListView) { // DÜZELTME: Strict eşleşme — includes() yerine === kullan. // includes('Hız') yanlış satırları yakalayabilir, sadece ilk td'ye bak. $skillRow = $skillTable.find('tr').filter(function () { const $row = $(this); const $clippable = $row.find('.clippable'); const $skillName = $row.find('.skill_name span:first-child'); const $firstTd = $row.find('td').first(); let rowSkillName = ''; if ($clippable.length > 0) rowSkillName = $clippable.text().trim(); else if ($skillName.length > 0) rowSkillName = $skillName.text().trim(); else rowSkillName = $firstTd.text().trim().replace(/[¹²\d\s%]+$/, '').trim(); rowSkillName = rowSkillName.replace(/\s+/g, ' ').trim(); const trName = Core.i18n.TRANSLATIONS['tr'][skill.key]; const enName = Core.i18n.TRANSLATIONS['en'][skill.key]; return rowSkillName === trName || rowSkillName === enName; }).first(); } else { $skillRow = $skillTable.find('td').filter(function () { const $img = $(this).find('img').first(); if ($img.length === 0) return false; const alt = $img.attr('alt') || ''; const trName = Core.i18n.TRANSLATIONS['tr'][skill.key]; const enName = Core.i18n.TRANSLATIONS['en'][skill.key]; return alt.startsWith(trName) || alt.startsWith(enName); }).first(); } if (!$skillRow || $skillRow.length === 0) { completedSkills++; this.checkAllSkillsComplete(pid, $container, completedSkills, skills.length, trainingData); continue; } // Skip if maxed or already injected if ($skillRow.find('.skill_exact2').length > 0) { completedSkills++; this.checkAllSkillsComplete(pid, $container, completedSkills, skills.length, trainingData); continue; } let isMaxed = false; if ($skillRow.attr('data-mz-maxed') === 'true') isMaxed = true; const $skillValueTd = $skillRow.find('td.skillval'); if (!isMaxed && $skillValueTd.find('.maxed').length > 0) isMaxed = true; if (!isMaxed) { const IMG_RED = "data:image/gif;base64,R0lGODlhDAAKAJEDAP////8AAMyZmf///yH5BAEAAAMALAAAAAAMAAoAAAIk3BQZYp0CAAptxvjMgojTEVwKpl0dCQrQJX3T+jpLNDXGlDUFADs="; if ($skillValueTd.find(`img[src="${IMG_RED}"]`).length > 0) isMaxed = true; } if (!isMaxed) { const $valSpan = $skillValueTd.find('span'); const color = $valSpan.css('color'); // FIX: Eğer kırmızı renk gm_s1 sınıfından geliyorsa (DP=1), bu tıkalı olduğu anlamına gelmez. if ((color === 'rgb(255, 0, 0)' || color === 'red' || $skillValueTd.find('font[color="red"]').length > 0) && !$skillRow.hasClass('gm_s1') && !$skillRow.find('.gm_s1').length) { isMaxed = true; } } // SADECE Hız (0) ve Dayanıklılık (1) için tıkalı olsa dahi gerileme araştırması yap (MZ Aging Deterioration), diğerlerinde atla if (isMaxed && skill.index > 1) { completedSkills++; this.checkAllSkillsComplete(pid, $container, completedSkills, skills.length, trainingData); continue; } const result = await this.getSkillTrainingData(pid, skill.index); if (result) { // 1. IMMEDIATELY INJECT PERCENTAGE this.injectPercentageToRow($skillRow, result.percent, pid, skill.index); trainingData[skill.index] = result; } completedSkills++; this.checkAllSkillsComplete(pid, $container, completedSkills, skills.length, trainingData); // Kademeli gecikme (network throttle) - tarayıcıyı yormamak için await new Promise(r => setTimeout(r, 150)); } } } catch (e) { console.error('[TrainingModule] processSinglePlayer Error:', e); } }, checkAllSkillsComplete(pid, $container, current, total, trainingData) { if (current < total) return; // All skills done for this player this.updateGeneralEfficiency(pid, $container); $container.addClass('mz-training-finished'); // Dashboard'u hemen tetikle (Hızlandırma) if (typeof window._triggerDashboardUpdate === 'function') { window._triggerDashboardUpdate(); } // Save to Cache if (Object.keys(trainingData).length > 0) { const skillBalls = {}; $container.find('table.player_skills tr').each(function () { const name = $(this).find('.clippable, .skill_name span:first-child').first().text().trim(); const balls = $(this).find('img[src*="skillball"]').length; if (name) skillBalls[name] = balls; }); PlayerDataCache.savePlayerData(pid, { training: trainingData, skillBalls: skillBalls, generalEfficiency: $container.find('.genel-verim').text().replace(/[^0-9.]/g, '') }); // Update Progress this.updateProgress(); } }, injectPercentageToRow($skillRow, percentStr, pid, skillIndex) { if ($skillRow.find('.skill_exact2').length > 0) return; if (percentStr === "") return; // Tıkalı ama gerilemesi olmayanlarda 0% gösterme const isTable = $skillRow.is('tr'); const percentNum = parseFloat(percentStr); const $percentElem = $(`<span id="pid_${pid}_skill_${skillIndex}" class="skillval skill_exact_van">${percentStr}%</span>`); $percentElem.css('font-weight', percentNum < 0 ? 'bold' : 'normal'); if (percentNum < 0) $percentElem.css('color', '#d32f2f'); if (isTable) { const $newTd = $('<td class="skill_exact2"></td>'); const $div = $('<div></div>').append($percentElem); $newTd.append($div); $skillRow.append($newTd); } else { const $divContainer = $('<div class="skill_exact2 clearfix" style="display:inline-block; margin-left: 5px; vertical-align: middle;"></div>'); const $innerDiv = $('<div></div>').append($percentElem); $divContainer.append($innerDiv); $skillRow.append($divContainer); } }, updateGeneralEfficiency(pid, $container) { const skills = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let totalEff = 0; let count = 0; skills.forEach(idx => { const cacheKey = `${pid}_${idx}`; const data = this.cache.get(cacheKey); if (data && data.efficiency > 0) { totalEff += data.efficiency; count++; } }); if (count > 0) { const avgEff = (totalEff / count).toFixed(2); this.injectEfficiencyLabel($container, avgEff); } }, injectEfficiencyLabel($container, avgEff) { const color = this.getEfficiencyColor(avgEff); const pid = this.getPid($container); if (!pid) return; // Find the ID span and its container more robustly let $idSpan = $container.find(`.player_id_span`).closest('.floatRight'); if ($idSpan.length === 0) { // Fallback: search the entire document for this specific player's ID span $idSpan = $(`.player_id_span`).filter(function () { return $(this).text().includes(pid); }).closest('.floatRight'); } if ($idSpan.length > 0) { // Update existing or create new const $existing = $idSpan.find('.genel-verim'); if ($existing.length > 0) { $existing.css('color', color).text(`${Core.i18n.getText('genelVerim')}: ${avgEff} ${Core.i18n.getText('birimGun')}`); $idSpan.find('.mz-reset-cache').attr('data-pid', pid); return; } // İlk kez oluştur $idSpan.find('.genel-verim-container').remove(); $idSpan.prepend(` <div class="genel-verim-container" style="display:inline-flex; align-items:center; margin-right:15px;"> <span class="genel-verim" style="font-weight:bold; font-size:12px; color:${color}; text-shadow: 0 0 2px rgba(0,0,0,0.5);"> ${Core.i18n.getText('genelVerim')}: ${avgEff} ${Core.i18n.getText('birimGun')} </span> <span class="mz-reset-cache" data-pid="${pid}" title="${Core.i18n.getText('updateTraining')}" style="cursor:pointer; margin-left:6px; font-size:14px; opacity:1; transition:opacity 0.2s;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=1"> 🔄 </span> </div> `); } }, // Fast restore from cache (called on AJAX refresh) restoreFromCache($container) { // 1. Daha güvenilir PID tespiti let pid = this.getPid($container); if (!pid) return false; // 2. Önbellekte veri var mı kontrol et const cachedData = PlayerDataCache.getPlayerData(pid) || this.cache.get(pid); if (!cachedData || !cachedData.training) return false; // 3. Tablo yapısının hazır olması beklenebilir (özellikle transfer sayfası AJAX'ta) let $skillTable = $container.is('table.player_skills') ? $container : $container.find('table.player_skills').first(); let isListView = false; if ($skillTable.length === 0) { if (window.location.href.includes('p=transfer')) { // Eğer transfer sayfasındaysak ve tablo henüz yoksa, worker'ın retry mekanizmasına bırak return false; } $skillTable = $container; isListView = true; } // 4. Zaten enjekte edilmiş mi kontrol et // DÜZELTME: Sadece bir kısmı enjekte edildiyse devam etmesine izin ver. // if ($skillTable.find('.skill_exact2').length > 0) return true; // 5. Şablon üzerinden enjeksiyon let injectedAnything = false; let incomplete = false; const skillsList = [ { key: 'skillSpeed', index: 0 }, { key: 'skillStamina', index: 1 }, { key: 'skillIntelligence', index: 2 }, { key: 'skillPassing', index: 3 }, { key: 'skillShooting', index: 4 }, { key: 'skillHeading', index: 5 }, { key: 'skillKeeping', index: 6 }, { key: 'skillControl', index: 7 }, { key: 'skillTackling', index: 8 }, { key: 'skillCrossing', index: 9 }, { key: 'skillSetPlays', index: 10 } ]; skillsList.forEach(skill => { let $skillRow; if (!isListView) { $skillRow = $skillTable.find('tr').filter(function () { const $row = $(this); const $clippable = $row.find('.clippable'); const $skillName = $row.find('.skill_name span:first-child'); const $firstTd = $row.find('td').first(); let rowSkillName = ''; if ($clippable.length > 0) rowSkillName = $clippable.text().trim(); else if ($skillName.length > 0) rowSkillName = $skillName.text().trim(); else rowSkillName = $firstTd.text().trim().replace(/[¹²\d\s%]+$/, '').trim(); const trName = Core.i18n.TRANSLATIONS['tr'][skill.key]; const enName = Core.i18n.TRANSLATIONS['en'][skill.key]; return rowSkillName === trName || rowSkillName === enName; }).first(); } else { $skillRow = $skillTable.find('td').filter(function () { const trName = Core.i18n.TRANSLATIONS['tr'][skill.key]; const enName = Core.i18n.TRANSLATIONS['en'][skill.key]; return $(this).find(`img[alt^="${trName}"], img[alt^="${enName}"]`).length > 0; }).first(); } const trainingResult = cachedData.training[skill.index]; if (!trainingResult || trainingResult.percent === undefined) { // Tıkalı kontrolü let isMaxed = $skillRow ? ($skillRow.attr('data-mz-maxed') === 'true') : false; if ($skillRow && !isMaxed) { const $skillValueTd = $skillRow.find('td.skillval'); if ($skillValueTd.find('.maxed').length > 0) isMaxed = true; if (!isMaxed) { const IMG_RED = "data:image/gif;base64,R0lGODlhDAAKAJEDAP////8AAMyZmf///yH5BAEAAAMALAAAAAAMAAoAAAIk3BQZYp0CAAptxvjMgojTEVwKpl0dCQrQJX3T+jpLNDXGlDUFADs="; if ($skillValueTd.find(`img[src="${IMG_RED}"]`).length > 0) isMaxed = true; } } // Hız/Dayanıklılık tıkalı olsa bile gerileme gösterilmeli if (!isMaxed || skill.index <= 1) incomplete = true; return; } if ($skillRow && $skillRow.length > 0) { // Crucial: Fill the internal memory cache so tooltips and General Efficiency calculation work after restore const cacheKey = `${pid}_${skill.index}`; if (!this.cache.has(cacheKey)) { this.cache.set(cacheKey, trainingResult); } this.injectPercentageToRow($skillRow, trainingResult.percent, pid, skill.index); injectedAnything = true; } }); // Recalculate and inject General Efficiency label after restore if (injectedAnything) { this.updateGeneralEfficiency(pid, $container); } $container.addClass('mz-training-finished'); // Önbellekten geldiyse de hemen tetikle if (typeof window._triggerDashboardUpdate === 'function') { window._triggerDashboardUpdate(); } return injectedAnything && !incomplete; }, setupStyles() { GM_addStyle(` .mz-training-tooltip { position: absolute; background: rgba(30, 30, 30, 0.95); border: 1px solid #444; border-radius: 8px; padding: 12px; color: #fff; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 12px; z-index: 10000; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5); min-width: 280px; max-width: 350px; backdrop-filter: blur(5px); pointer-events: none; display: none; } .mz-tooltip-compact { padding: 8px; min-width: 240px; max-width: 300px; font-size: 11px; } .mz-tooltip-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #555; } .mz-tooltip-subheader { font-size: 10px; text-transform: uppercase; color: #bbb; margin-bottom: 4px; letter-spacing: 0.5px; } .mz-tooltip-row { display: flex; justify-content: space-between; align-items: center; padding: 2px 0; } .mz-badge { display: inline-block; padding: 1px 5px; border-radius: 3px; font-size: 10px; min-width: 18px; text-align: center; margin-right: 6px; } .mz-badge-gray { background: #555; color: #ccc; } .mz-badge-yellow { background: #ffca28; color: #222; } .mz-badge-green { background: #66bb6a; color: #fff; } .mz-badge-blue { background: #29b6f6; color: #fff; } .mz-tooltip-footer { margin-top: 10px; padding-top: 8px; border-top: 1px solid #444; font-size: 11px; color: #ccc; } .mz-reset-cache { cursor: pointer; font-size: 14px; transition: transform 0.2s; } .mz-reset-cache:hover { transform: rotate(30deg); } .mz-notification-toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.9); color: #4caf50; padding: 12px 24px; border-radius: 8px; font-size: 14px; font-weight: bold; z-index: 100000; box-shadow: 0 4px 12px rgba(0,0,0,0.5); opacity: 0; transition: opacity 0.3s, transform 0.3s; pointer-events: none; display: flex; align-items: center; border: 1px solid #4caf50; } .mz-notification-toast.show { opacity: 1; transform: translateX(-50%) translateY(10px); } `); }, // Notification Helper showNotification(message, duration = 3000) { let $toast = $('.mz-notification-toast'); if ($toast.length === 0) { $toast = $(`<div class="mz-notification-toast"></div>`).appendTo('body'); } $toast.html(` <span style="font-size:18px; margin-right:8px;">✅</span> <span>${message}</span> `); // Force reflow $toast[0].offsetHeight; $toast.addClass('show'); if (this.notificationTimeout) clearTimeout(this.notificationTimeout); if (duration > 0) { this.notificationTimeout = setTimeout(() => { $toast.removeClass('show'); }, duration); } }, updateProgress() { if (!this.progress.active) return; this.progress.current++; if (this.progress.current >= this.progress.total) { // Completed! this.showNotification(`Tüm antreman verileri güncellendi! ✅`, 3000); // Reset this.progress = { total: 0, current: 0, active: false }; } else { // Still processing // this.showNotification(`Veriler işleniyor... (${this.progress.current}/${this.progress.total})`, 0); } }, setupObservers() { // Initialize URL change observer for SPA-like navigation let lastUrl = window.location.href; // Debounce function to prevent excessive calls let timeoutId; const debouncedProcess = () => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { this.processVisiblePlayers(); }, 250); }; // URL değişimlerini ve yeni DOM elementlerini basit bir aralıkla kontrol et // URL değişimlerini ve yeni DOM elementlerini basit bir aralıkla kontrol et setInterval(() => { const currentUrl = window.location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; console.log('[TrainingModule] URL değişti, tarama tetikleniyor...'); debouncedProcess(); } else { // JANK Önleyici: Sadece yeni oyuncu container'ı eklendiyse tarama yap const players = document.querySelectorAll('.playerContainer, table.player_skills'); const currentHash = players.length + "_" + (players[0]?.innerText.substring(0,10) || ""); if (window._lastTrainingHash === currentHash) return; window._lastTrainingHash = currentHash; debouncedProcess(); } }, 1500); // Global AJAX Listener (jQuery) if (typeof $ !== 'undefined') { $(document).ajaxComplete((event, xhr, settings) => { const url = settings.url || ""; // search, filter, scout_report veya transfer parametrelerini içeren AJAX türlerini yakala const isRelevant = (url.includes('p=transfer') || url.includes('p=players') || url.includes('sub=scout_report') || url.includes('sub=search') || url.includes('sub=filter')) && !url.includes('sub=monitor') && !url.includes('sub=buy') && !url.includes('sub=bid'); if (isRelevant) { // console.log('[TrainingModule] Relevant AJAX detected:', url); debouncedProcess(); // Bazı aramaların yüklenmesi uzun sürebilir, yedek tetiklemeler ekleyelim setTimeout(debouncedProcess, 1500); setTimeout(debouncedProcess, 4000); } }); // Transfer sayfasındaki 'Ara' butonu (Span bazlı buton yapısı dahil) için özel dinleyici $(document).on('click', '.buttonClassMiddle, .buttonContent, .buttonClassRight, .buttonClassLeft, #transfer_search_button, #searchb, input[value*="Ara"], input[value*="Search"], i.fa-rocket', function () { const btnText = $(this).text().trim().toLowerCase(); if (btnText.includes('ara') || btnText.includes('search') || $(this).closest('#searchb').length > 0) { console.log('[TrainingModule] Arama butonu tıklandığı algılandı.'); setTimeout(debouncedProcess, 500); setTimeout(debouncedProcess, 1500); setTimeout(debouncedProcess, 3000); setTimeout(debouncedProcess, 6000); } }); } // MZ'nin submitAjaxSearch fonksiyonunu yakala (EN GÜVENİLİR YÖNTEM) if (typeof unsafeWindow.submitAjaxSearch === 'function') { const originalSubmit = unsafeWindow.submitAjaxSearch; console.log('[TrainingModule] submitAjaxSearch takip altına alındı.'); unsafeWindow.submitAjaxSearch = function () { console.log('[TrainingModule] submitAjaxSearch tetiklendi (AJAX Araması Başlıyor).'); setTimeout(debouncedProcess, 1000); setTimeout(debouncedProcess, 2500); setTimeout(debouncedProcess, 5000); return originalSubmit.apply(this, arguments); }; } }, init() { try { this.setupStyles(); this.setupObservers(); this.checkDailyAutoClear(); // Check for daily reset } catch (e) { console.error('[TrainingModule] Init Error:', e); } }, checkDailyAutoClear() { const now = new Date(); const currentHour = now.getHours(); // Only trigger if it's past 03:00 AM if (currentHour >= 3) { // We use local date string as key for "today" // e.g. "20.02.2026" const todayStr = now.toLocaleDateString('tr-TR'); const lastCleared = GM_getValue('mz_training_last_cleared_date', ''); if (lastCleared !== todayStr) { // console.log(`[TrainingModule] Daily 03:00 AM check: New day detected (${todayStr}). Clearing cache...`); // Clear Persistent Cache if (typeof PlayerDataCache !== 'undefined') { if (PlayerDataCache.clearAll) { PlayerDataCache.clearAll(); } else if (PlayerDataCache.cache) { PlayerDataCache.cache.clear(); if (PlayerDataCache.persist) PlayerDataCache.persist(); } } // Clear Internal Memory Cache if (this.cache) this.cache.clear(); // Save new date GM_setValue('mz_training_last_cleared_date', todayStr); // Notify User this.showNotification(`Yeni gün (${todayStr}): Antrenman verileri sıfırlandı.`); } } }, setupHooks() { // Not: centerPowerbox ve ajaxSubmit hookları kaldırıldı. // Çünkü bu hooklar bazı popupların (paketler vb.) içeriğinin boş gelmesine neden oluyordu. // Artık MutationObserver ve ajaxComplete global dinleyicileri yeterli. } }; // Initialize the training percentage module TrainingModule.init(); // Initial Run setTimeout(() => { TrainingModule.processVisiblePlayers(); TrainingModule.setupHooks(); // Initialize hooks }, 1500); if (typeof $ !== 'undefined') { // 5. Tooltip Event Listeners (Delegated) - Redesigned let $tooltip = null; // Helper to get color const getColorForLevel = (lvl) => { if (lvl >= 7) return '#3399ff'; // Blue if (lvl >= 4) return '#00cc66'; // Green if (lvl >= 2) return '#ffcc00'; // Yellow return '#aaaaaa'; // Gray }; // Helper to generate rows for a stat object const generateRows = (statObj, targetLevel = null) => { let rowsHtml = ''; const coeffs = { t1: 0.645, t2: 0.55, t3: 0.7, t4: 0.85, t5: 0.96, t6: 1.111, t7: 1.3, t8: 1.6, t9: 2.02, t10: 2.4 }; let totalUnits = 0; let totalDays = 0; let maxLvl = 0; // Iterate 10 down to 1 for (let lvl = 10; lvl >= 1; lvl--) { const count = statObj[`t${lvl}`] || 0; if (count > 0) { // totalUnits = count * level * coeff totalUnits += count * lvl * (coeffs[`t${lvl}`] || 1); totalDays += count; if (lvl > maxLvl) maxLvl = lvl; // Visual Bars let barsHtml = ''; const bColor = TrainingModule.getColorForLevel(lvl); for (let i = 0; i < lvl; i++) { barsHtml += `<div style="width:6px; height:10px; background-color:${bColor}; margin-left:1px; border-radius:1px;"></div>`; } // Badge Color let badgeClass = 'mz-badge-gray'; if (lvl >= 7) badgeClass = 'mz-badge-blue'; else if (lvl >= 4) badgeClass = 'mz-badge-green'; else if (lvl >= 2) badgeClass = 'mz-badge-yellow'; let dotHtml = ''; if (targetLevel !== null && lvl === targetLevel) { dotHtml = `<span style="display:inline-block; width:8px; height:8px; background-color:#39ff14; border-radius:50%; margin-left:6px; box-shadow:0 0 6px #39ff14, 0 0 10px #39ff14; flex-shrink:0;" title="En son çalışılan seviye"></span>`; } rowsHtml += ` <div class="mz-tooltip-row"> <div style="display:flex; align-items:center;"> <span class="mz-badge ${badgeClass}">${lvl}</span> <span style="color:#ddd; font-size:11px; display:inline-flex; align-items:center; margin-left:5px;">Seviye${dotHtml}</span> </div> <div style="display:flex; align-items:center;"> <span style="color:${bColor}; font-weight:bold; margin-right:6px;">${count} Gün</span> <div style="display:flex;">${barsHtml}</div> </div> </div> `; } } return { html: rowsHtml, totalUnits: parseFloat(totalUnits.toFixed(1)), totalDays, maxLvl }; }; // Skill Names Mapping (Turkish) const skillNames = [ 'Hız', 'Dayanıklılık', 'Oyun Zekası', 'Paslaşma', 'Şut Çekme', 'Kafa Vuruşu', 'Kalecilik', 'Top Kontrolü', 'Top Çalma', 'Orta Yapma', 'Duran Top' ]; $(document).on('mouseenter', '.skill_exact_van', function (e) { const $target = $(this); // Smart Positioning & Compact Mode const moveTooltip = (e) => { if (!$tooltip) return; // 1. Reset Compact Mode, max-height and widths to calculate natural dimensions $tooltip.removeClass('mz-tooltip-compact'); $tooltip.css({ 'width': '', 'min-width': '', 'max-width': '', 'height': '', 'position': 'fixed', 'top': '', 'left': 'auto', 'right': '20px', 'margin': '0' }); const $historyContainer = $tooltip.find('.mz-history-grid'); if ($historyContainer.length > 0) { $historyContainer.css({ 'max-height': '', 'width': '', 'min-width': '', 'display': '', 'flex-flow': 'column nowrap', 'gap': '5px' }); } const viewportHeight = $(window).height(); let tooltipHeight = $tooltip.outerHeight(); // 2. Boyut Ekranı aşıyorsa Grid kolonlarına böl (Soldan sağa sıralama) if ($historyContainer.length > 0 && tooltipHeight > viewportHeight - 40) { const historyOuterHeight = $historyContainer.outerHeight(); const nonHistoryHeight = tooltipHeight - historyOuterHeight; const historyMax = Math.max(80, viewportHeight - nonHistoryHeight - 30); // Soldan sağa sıralanması istendiğinden flex-direction: row wrap yapıyoruz. // Satır içi (row) sarmalama doğrudan kabın genişliğine duyarlı olduğu için, // genişliği artırmamız gerekiyor ki alt satıra geçmek için alan bulsun. // Önce, kutuların genişliğini tespit edelim const colW = $historyContainer.children().first().outerWidth(true) || 220; const cGap = 15; const rGap = 5; // row gap const childrenCount = $historyContainer.children().length; // İhtiyaç duyulan toplam yüksekliği satır sayısına göre bulmak yerine // kaç sütun yaparsak (genişliği ne kadar artırırsak) historyMax yüksekliğe sığar? let MathCols = 1; let requiredHeight = childrenCount * (60 + rGap); // kaba taslak ilk yükseklik // Optimizasyon: sığana kadar sütun sayısını (MathCols) artır while (MathCols < childrenCount) { let currR = 0; let maxRowH = 0; let totalH = 0; let colIndex = 0; $historyContainer.children().each(function () { const iH = $(this).outerHeight(true) || 60; maxRowH = Math.max(maxRowH, iH); colIndex++; if (colIndex >= MathCols) { totalH += maxRowH + rGap; colIndex = 0; maxRowH = 0; } }); if (colIndex > 0) totalH += maxRowH + rGap; // son satır eklentisi if (totalH <= historyMax) { break; // Sığdığı sütun sayısını bulduk } MathCols++; } const requiredWidth = (MathCols * colW) + ((MathCols - 1) * cGap); $historyContainer.css({ 'max-height': historyMax + 'px', 'display': 'flex', 'flex-flow': 'row wrap', // Soldan sağa! 'align-content': 'flex-start', 'gap': '5px 15px', 'width': requiredWidth + 'px', 'min-width': requiredWidth + 'px' }); const padding = 24; // 12px padding $tooltip.css({ 'width': (requiredWidth + padding) + 'px', 'min-width': (requiredWidth + padding) + 'px' }); tooltipHeight = $tooltip.outerHeight(); } // 3. Yatay ve Dikey Hizalama let targetTop = e.clientY + 15; let targetLeft = e.clientX + 15; const viewportWidth = $(window).width(); let tooltipWidth = $tooltip.outerWidth(); // Ekrana sağdan sığmıyorsa imlecin soluna al if (targetLeft + tooltipWidth > viewportWidth - 20) { targetLeft = e.clientX - tooltipWidth - 15; } // Sola da sığmıyorsa (çok genişse) ekranın en solundan başlat if (targetLeft < 10) { targetLeft = 10; } if (targetTop + tooltipHeight > viewportHeight - 10) { targetTop = viewportHeight - tooltipHeight - 10; } if (targetTop < 10) { targetTop = 10; } if (tooltipHeight > viewportHeight - 20) { $tooltip.addClass('mz-tooltip-compact'); } $tooltip.css({ 'top': targetTop + 'px', 'left': targetLeft + 'px', 'right': 'auto', 'display': 'block' }); }; const id = $target.attr('id'); if (!id) return; const parts = id.split('_'); const pid = parts[1]; const skillIndex = parseInt(parts[3]); // Ensure int for array lookup const cacheKey = `${pid}_${skillIndex}`; const data = TrainingModule.cache.get(cacheKey); if (!data || parseFloat(data.percent) < 0) return; // Determine overall latest training time across all skills for this player let overallLastTime = 0; for (let i = 0; i <= 10; i++) { const sData = TrainingModule.cache.get(`${pid}_${i}`); if (sData && sData.lastTrainedTime) { if (sData.lastTrainedTime > overallLastTime) { overallLastTime = sData.lastTrainedTime; } } } let targetLevel = null; if (data.lastTrainedTime && data.lastTrainedTime === overallLastTime && overallLastTime > 0) { targetLevel = data.lastTrainedLevel; } if (!$tooltip) { $tooltip = $('<div class="mz-training-tooltip"></div>').appendTo('body'); } const pct = parseFloat(data.percent); const isNegative = pct < 0; // Get Skill Name const skillName = skillNames[skillIndex] || 'Bilinmeyen'; // --- HISTORICAL DATA CALCULATIONS --- let totalPastUnits = 0; let totalPastDays = 0; let validPastSegments = 0; const pastItems = []; let pastIndex = 1; if (data.segments && data.segments.length > 1) { for (let i = data.segments.length - 2; i >= 0; i--) { const seg = data.segments[i]; const segSum = TrainingModule.getSum(seg.stat); const segData = generateRows(seg.stat); if (segData.totalDays > 0) { validPastSegments++; totalPastUnits += segSum; totalPastDays += segData.totalDays; pastItems.push(` <div style="background-color:rgba(255,255,255,0.03); padding:4px; border-radius:4px; margin-bottom:4px;"> <div style="margin-bottom:2px; font-weight:bold; color:#aaa; font-size:10px; border-bottom:1px dotted #444;">${pastIndex}. Önceki Top</div> ${segData.html} <div style="text-align:right; font-size:9px; color:#ccc; margin-top:2px;"> Toplam: <b>${segData.totalDays} Gün</b> - <b>${segData.totalUnits} Birim</b> </div> </div> `); pastIndex++; } } } // --- PERCENTAGE ADJUSTMENT (Dynamic Goal NOT used for display anymore) --- // We use fixed 100-unit goal for display percentage as requested per van.mz logic. const currentUnits = TrainingModule.getSum(data.details); let displayPercent = data.percent; // For estimation, we still use the historical average goal if available // because it gives more realistic "days left" even if the display % is smaller. let realisticGoalUnits = 100; if (validPastSegments > 0) { realisticGoalUnits = totalPastUnits / validPastSegments; } let html = ` <div class="mz-tooltip-header"> <div style="display:flex; flex-direction:column; line-height:1.2;"> <span style="color:#e040fb; font-size:12px; font-weight:bold; text-transform:uppercase; letter-spacing:0.5px;">${skillName}</span> <span style="color:#aaa; font-size:10px; font-weight:normal;">İlerleme</span> </div> <span style="color:#ffcc00; font-size:15px;">${displayPercent}%</span> </div> `; // --- CURRENT PROGRESS --- // We use data.details (which is currentSegment.stat) const currentData = generateRows(data.details, targetLevel); // (Historical segments moved up) // Efficiency Calculation (Using units for consistency with percentage) let grandTotalUnits = totalPastUnits; let grandTotalDays = 0; // Sum all days from all segments data.segments.forEach(seg => { const segData = generateRows(seg.stat); grandTotalDays += segData.totalDays; }); const efficiencyVal = data.efficiency || 0; const efficiencyDisplay = efficiencyVal.toFixed(2); const effColor = TrainingModule.getEfficiencyColor(efficiencyDisplay); // --- ESTIMATION LOGIC (For Header) --- const currentEff = (currentData.totalDays > 0) ? (currentData.totalUnits / currentData.totalDays) : (efficiencyVal || 0); let estimationStr = ''; if (currentEff > 0 && displayPercent > 0 && displayPercent < 99.9) { const remainingUnits = Math.max(0, realisticGoalUnits - currentUnits); const daysLeft = remainingUnits / currentEff; const daysLeftStr = Math.ceil(daysLeft); estimationStr = `<span style="font-weight:normal; color:#bbb; font-size:10px; margin-left:4px;">(Tahmini: ~${daysLeftStr} Gün)</span>`; } // --- TREND LOGIC --- let trendIcon = ''; if (currentData.totalDays > 0 && efficiencyVal > 0) { const currentKg = currentData.totalUnits / currentData.totalDays; const threshold = 0.10; // 10% tolerance if (currentKg > efficiencyVal * (1 + threshold)) { trendIcon = '<span style="color:#4caf50; font-weight:bold;">▲</span>'; // Green Up } else if (currentKg < efficiencyVal * (1 - threshold)) { trendIcon = '<span style="color:#f44336; font-weight:bold;">▼</span>'; // Red Down } else { trendIcon = '<span style="color:#888;">➖</span>'; // Gray Dash } } // --- GENERATE HTML --- if (currentData.totalDays > 0) { html += ` <div class="mz-tooltip-subheader">BU TOP İÇİN ÇALIŞMA ${estimationStr}</div> ${currentData.html} <div style="text-align:right; font-size:10px; color:#ddd; margin-top:2px; background-color:rgba(255,255,255,0.05); padding:2px 4px; border-radius:2px;"> Toplam: <b>${currentData.totalDays} Gün</b> - <b>${currentData.totalUnits.toFixed(1)} Birim</b> - <b>${currentEff.toFixed(2)} Birim/G</b><span style="margin-left:6px; font-size:12px;">${trendIcon}</span> </div> `; } else { html += `<div style="font-style:italic; color:#777; font-size:11px; padding:4px 8px;">Henüz veri yok.</div>`; } // Append Past Segments HTML if (pastItems.length > 0) { html += `<div class="mz-tooltip-subheader" style="margin-top:8px; border-top:1px solid #444; padding-top:4px;">GEÇMİŞ TOP KAZANIMLARI</div>`; if (pastItems.length > 3) { html += `<div class="mz-history-grid">${pastItems.join('')}</div>`; } else { html += `<div class="mz-history-list" style="display:flex; flex-direction:column;">${pastItems.join('')}</div>`; } } // Footer (Efficiency & Average) if (validPastSegments > 0 || currentData.totalDays > 0) { const avgUnits = (validPastSegments > 0) ? (totalPastUnits / validPastSegments).toFixed(1) : "-"; const avgDays = (validPastSegments > 0) ? (totalPastDays / validPastSegments).toFixed(1) : "-"; html += ` <div class="mz-tooltip-footer"> <div style="display:flex; justify-content:space-between; align-items:center;"> <div> <span style="font-size:14px; margin-right:4px;">ℹ️</span> <span>Ortalama: <b>~${avgDays} Gün</b> / <b>${avgUnits} Birim</b></span> </div> <div style="margin-left: 15px; padding-left: 15px; border-left: 1px solid #555; color: ${effColor}; font-weight: bold;"> ${efficiencyDisplay} Birim/G </div> </div> </div> `; } $tooltip.html(html).show(); // Initial positioning moveTooltip(e); $(document).on('mousemove.mz_training', moveTooltip); }); $(document).on('mouseleave', '.skill_exact_van', function () { if ($tooltip) $tooltip.hide(); $(document).off('mousemove.mz_training'); }); // --- Cache Reset Logic --- $(document).off('click.mz_reset').on('click.mz_reset', '.mz-reset-cache', function (e) { e.preventDefault(); const $btn = $(this); const pid = $btn.attr('data-pid'); if (!pid) return; // console.log(`[TrainingModule] Resetting cache for PID: ${pid}`); // 1. Clear caches if (PlayerDataCache && PlayerDataCache.cache) { PlayerDataCache.cache.delete(pid); PlayerDataCache.persist(); } // Clear TrainingModule's internal memory cache for this player for (let i = 0; i <= 10; i++) { TrainingModule.cache.delete(`${pid}_${i}`); } // 2. UI Feedback — sadece yüzde etiketlerini sil, Genel Verim'e dokunma const $container = $btn.closest('.playerContainer'); if ($container.length === 0) { const $parent = $btn.closest('div, td, table, .player_container').parent(); $parent.find('.skill_exact2').remove(); $parent.find('.mz-aging-deterioration').remove(); TrainingModule.processSinglePlayer($parent); if (window.mzAgingRefresh) window.mzAgingRefresh(pid, $parent[0]); } else { $container.find('.skill_exact2').remove(); $container.find('.mz-aging-deterioration').remove(); TrainingModule.processSinglePlayer($container); if (window.mzAgingRefresh) window.mzAgingRefresh(pid, $container[0]); } // Visual feedback on the button $btn.text('⌛').css('pointer-events', 'none'); setTimeout(() => { $btn.text('🔄').css('pointer-events', 'auto'); }, 2000); }); } // console.log('[Training Percentage] Module fully loaded'); } // Initialize the training percentage module // owned by legacy-player-advanced // ========================================== // 8. QUICK ACCESS MENU (HIZLI ERİSİM MENUSU) // ========================================== function initializeQuickAccessMenu() { // 1. Inject CSS const quickMenuCSS = ` #mz-quick-menu-container { position: fixed; top: calc(50% - 110px); left: -270px; transform: translateY(-50%); width: 270px; background-color: #1e1e1e; border: 2px solid #fff; border-left: none; border-radius: 0 10px 10px 0; box-shadow: 10px 0 30px rgba(0,0,0,0.7); transition: left 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); z-index: 999999; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; color: #fff; } #mz-quick-menu-container.active { left: 0; } #mz-quick-menu-toggle { position: absolute; top: 50%; right: -32px; transform: translateY(-50%); width: 32px; height: 70px; background: #00529B; color: white; display: flex; align-items: center; justify-content: center; font-size: 20px; cursor: pointer; border-radius: 0 10px 10px 0; border: 2px solid #fff; border-left: none; box-shadow: 4px 0 10px rgba(0,0,0,0.4); transition: all 0.3s ease; opacity: 0.9; } #mz-quick-menu-toggle:hover { opacity: 1; right: -35px; } #mz-quick-menu-header { padding: 12px; background: #00529B; border-bottom: 2px solid rgba(255,255,255,0.1); border-radius: 0 9px 0 0; text-align: center; font-weight: 800; font-size: 13px; color: #FFD700; text-transform: uppercase; letter-spacing: 1px; text-shadow: 0 1px 2px rgba(0,0,0,0.3); position: relative; } .mz-quick-menu-close-btn { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); cursor: pointer; color: #fff; font-size: 18px; font-weight: bold; display: none; } @media (max-width: 900px) { .mz-quick-menu-close-btn { display: block; } } #mz-quick-links-list { list-style: none; padding: 10px 8px; margin: 0; max-height: 280px; overflow-y: auto; } #mz-quick-links-list li { display: flex; align-items: center; margin-bottom: 8px; background: #252525; border-left: 3px solid #00529B; border-radius: 4px; transition: all 0.2s ease; box-shadow: 0 1px 3px rgba(0,0,0,0.2); } #mz-quick-links-list li:hover { background: #2e2e2e; border-left-width: 6px; transform: translateX(-2px); } #mz-quick-links-list a { color: #00ff00; /* Eklenen linkler yeşil */ text-decoration: none; padding: 8px 10px; flex-grow: 1; font-size: 11px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; } #mz-quick-links-list a:hover { color: #fff; text-shadow: 0 0 5px rgba(0,255,0,0.5); } .mz-delete-link-btn { background: rgba(0,0,0,0.2); border: none; color: #ff0000; /* X simgesi kırmızı */ cursor: pointer; padding: 0 12px; font-size: 20px; /* Büyütülmüş boyut */ font-weight: bold; line-height: 32px; border-radius: 0 4px 4px 0; transition: all 0.2s; } .mz-delete-link-btn:hover { color: #fff; background: #ff0000; } #mz-quick-menu-status { padding: 6px; font-size: 10px; text-align: center; background: #1a1a1a; color: #88ff88; border-top: 1px solid #333; display: none; } #mz-add-link-form { padding: 15px; background: #151515; border-top: 1px solid #333; border-radius: 0 0 10px 0; display: flex; flex-direction: column; gap: 12px; } .mz-add-group { border: 1px dashed #444; padding: 10px; border-radius: 6px; background: rgba(255,255,255,0.02); display: flex; flex-direction: column; gap: 8px; } .mz-add-label { font-size: 10px; color: #888; text-transform: uppercase; margin-bottom: -4px; font-weight: bold; } #mz-add-link-form input { width: 100%; padding: 8px; box-sizing: border-box; background: #2a2a2a; border: 1px solid #444; color: #ff0000; /* Eklenebilir link ismi kırmızı */ border-radius: 4px; font-size: 12px; font-weight: bold; outline: none; } #mz-add-link-form input:focus { border-color: #00529B; } #mz-add-link-btn { width: 100%; padding: 10px; background: #00529B; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 12px; transition: all 0.3s; box-shadow: 0 2px 5px rgba(0,0,0,0.3); } #mz-add-link-btn:hover { background: #0066c2; transform: translateY(-1px); } /* Hızlı Erişim açıkken alttaki butonun çakışmaması için kaydırma */ body.mz-quick-menu-active #mz-global-info-btn { transform: translateY(140px) !important; } body.mz-quick-menu-active #mz-global-info-btn:hover { transform: translateY(140px) scale(1.1) !important; } #mz-add-link-btn:hover { background-color: #003d73; } /* Custom Scrollbar */ #mz-quick-links-list::-webkit-scrollbar { width: 6px; } #mz-quick-links-list::-webkit-scrollbar-track { background: #2b2b2b; } #mz-quick-links-list::-webkit-scrollbar-thumb { background: #555; border-radius: 3px; } #mz-quick-links-list::-webkit-scrollbar-thumb:hover { background: #00529B; } /* === MOBİL: Hızlı Erişim Menüsü Kompakt Tab === */ @media (max-width: 900px) { /* Toggle butonu mobilde küçük, sol kenara yapışık dikey tab */ #mz-quick-menu-toggle { width: 24px !important; height: 52px !important; font-size: 14px !important; right: -24px !important; opacity: 0.85 !important; border-radius: 0 8px 8px 0 !important; } #mz-quick-menu-toggle:hover, #mz-quick-menu-toggle:active { right: -26px !important; opacity: 1 !important; } /* Panel açıkken tam genişlik ve kaydırılabilir */ #mz-quick-menu-container.active { left: 0 !important; max-height: 85vh; overflow-y: auto; } /* Link listesi mobilde biraz daha küçük */ #mz-quick-links-list a { font-size: 12px !important; padding: 10px 8px !important; } .mz-delete-link-btn { font-size: 18px !important; padding: 0 10px !important; } } `; $("<style>").prop("type", "text/css").html(quickMenuCSS).appendTo("head"); // --- Dil Desteği --- const i18n = { tr: { toggleTitle: "Hızlı Erişim Menüsü", header: "Hızlı Erişim", statusSuccess: "✅ Sayfa başarıyla eklendi!", newShortcutLabel: "📌 Yeni Kısayol:", addBtn: "⬇️ Bu Sayfayı Ekle", placeholder: "Kısayol başlığı yazın", emptyList: "Henüz kısayol eklenmedi.", delete: "Sil", defaultName: "Yeni Kısayol" }, en: { toggleTitle: "Quick Access Menu", header: "Quick Access", statusSuccess: "✅ Page added successfully!", newShortcutLabel: "📌 New Shortcut:", addBtn: "⬇️ Add This Page", placeholder: "Enter shortcut title", emptyList: "No shortcuts added yet.", delete: "Delete", defaultName: "New Shortcut" } }; const lang = Core.i18n.lang; const getText = (key) => i18n[lang][key] || i18n['en'][key]; // 2. Inject HTML const menuHTML = ` <div id="mz-quick-menu-container"> <div id="mz-quick-menu-toggle" title="${getText('toggleTitle')}">⭐</div> <div id="mz-quick-menu-header"> ${getText('header')} <span class="mz-quick-menu-close-btn">×</span> </div> <ul id="mz-quick-links-list"></ul> <div id="mz-quick-menu-status">${getText('statusSuccess')}</div> <div id="mz-add-link-form"> <div class="mz-add-group"> <div class="mz-add-label">${getText('newShortcutLabel')}</div> <input type="text" id="mz-new-link-name" placeholder="${getText('placeholder')}"> <button id="mz-add-link-btn">${getText('addBtn')}</button> </div> </div> </div> `; const quickMenu = $(menuHTML); window.__mzcMega.mountDonorContent("quick-access", quickMenu[0], getText('header')); // 3. State Management (LocalStorage) const storageKey = 'mz_quick_links_v1'; function getSavedLinks() { try { const stored = localStorage.getItem(storageKey); return stored ? JSON.parse(stored) : []; } catch (e) { console.error("Error loading quick links", e); return []; } } function saveLinks(links) { localStorage.setItem(storageKey, JSON.stringify(links)); } // 4. Render Logic function renderLinks() { const links = getSavedLinks(); const $list = $('#mz-quick-links-list'); $list.empty(); if (links.length === 0) { $list.append(`<li style="padding: 10px; color: #888; font-size: 11px; text-align: center;">${getText('emptyList')}</li>`); return; } links.forEach((link, index) => { const $li = $('<li></li>'); const $a = $('<a></a>').attr('href', link.url).text(link.name).attr('title', link.name); const $btn = $('<button></button>').addClass('mz-delete-link-btn').html('×').attr('data-index', index).attr('title', getText('delete')); $li.append($a).append($btn); $list.append($li); }); } // 5. Event Listeners const $container = $('#mz-quick-menu-container'); const $toggle = $('#mz-quick-menu-toggle'); const $addBtn = $('#mz-add-link-btn'); const $nameInput = $('#mz-new-link-name'); // Toggle visibility $toggle.on('click', function () { $container.toggleClass('active'); const isActive = $container.hasClass('active'); $('body').toggleClass('mz-quick-menu-active', isActive); if (isActive) { // Pre-fill input with a clean version of the title let currentTitle = document.title; currentTitle = currentTitle.replace(' - ManagerZone', '').trim(); $nameInput.val(currentTitle); } }); $('.mz-quick-menu-close-btn').on('click', function() { $container.removeClass('active'); $('body').removeClass('mz-quick-menu-active'); }); // Add current page $addBtn.on('click', function () { let name = $nameInput.val().trim(); const url = window.location.href; if (!name) { name = document.title.replace(' - ManagerZone', '').trim() || 'Yeni Kısayol'; } const links = getSavedLinks(); links.push({ name: name, url: url }); saveLinks(links); $nameInput.val(''); renderLinks(); // Visual feedback via status bar const $status = $('#mz-quick-menu-status'); $status.fadeIn().delay(2000).fadeOut(); }); // Delete link (Event Delegation) $('#mz-quick-links-list').on('click', '.mz-delete-link-btn', function () { const index = $(this).data('index'); const links = getSavedLinks(); if (index > -1 && index < links.length) { links.splice(index, 1); saveLinks(links); renderLinks(); } }); // Initial Render renderLinks(); } /**************************************************************************************** * * * BÖLÜM 16: OYUNCU ÖRÜMCEK GRAFİĞİ (RADAR CHART) - YENİ MODÜL * * * ****************************************************************************************/ function initializeRadarChartModule() { 'use strict'; const $ = jQuery; // Skill names for the radar chart const skillOrder = [ { id: 'speed', tr: 'Hız', en: 'Speed' }, { id: 'stamina', tr: 'Dayanıklılık', en: 'Stamina' }, { id: 'play_intelligence', tr: 'Oyun Zekası', en: 'Play Int.' }, { id: 'passing', tr: 'Paslaşma', en: 'Passing' }, { id: 'shooting', tr: 'Şut Çekme', en: 'Shooting' }, { id: 'heading', tr: 'Kafa Vuruşu', en: 'Heading' }, { id: 'keeping', tr: 'Kalecilik', en: 'Keeping' }, { id: 'ball_control', tr: 'Top Kontrolü', en: 'Ball Ctrl' }, { id: 'tackling', tr: 'Top Çalma', en: 'Tackling' }, { id: 'aerial_passing', tr: 'Orta Yapma', en: 'Aerial' }, { id: 'set_plays', tr: 'Duran Top', en: 'Set Plays' } ]; const lang = Core.i18n.lang; // Add Button Styles GM_addStyle(` .mz-radar-chart-btn { display: inline-block; width: 24px; height: 24px; background: #ffc107; border-radius: 4px; margin-left: 5px; cursor: pointer; text-align: center; line-height: 24px; color: #000; font-size: 14px; vertical-align: middle; transition: transform 0.2s, background 0.2s; border: 1px solid #e0a800; box-shadow: 1px 1px 3px rgba(0,0,0,0.2); } .mz-radar-chart-btn:hover { transform: scale(1.15); background: #ffca2c; } .mz-radar-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); z-index: 999999; display: none; justify-content: center; align-items: center; backdrop-filter: blur(5px); } .mz-radar-modal { background: #111e25; /* PES dark theme */ padding: 25px; border-radius: 12px; width: 550px; position: relative; box-shadow: 0 10px 40px rgba(0,0,0,0.8); border: 2px solid #2980b9; } #mz-radar-container { width: 100%; height: 450px; background: transparent; } .mz-radar-close { position: absolute; top: 10px; right: 15px; color: #ecf0f1; font-size: 28px; cursor: pointer; transition: color 0.2s; } .mz-radar-close:hover { color: #e74c3c; } .mz-radar-player-name { color: white; font-size: 18px; font-weight: bold; text-align: center; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 1px; } `); // Inject Modal if ($('#mz-radar-overlay').length === 0) { $('body').append(` <div id="mz-radar-overlay" class="mz-radar-modal-overlay"> <div class="mz-radar-modal"> <span class="mz-radar-close">×</span> <div class="mz-radar-player-name" id="mz-radar-modal-title">PLAYER STATS CHART</div> <div id="mz-radar-container"></div> </div> </div> `); $('#mz-radar-overlay .mz-radar-close, #mz-radar-overlay').on('click', function (e) { if (e.target === this || $(e.target).hasClass('mz-radar-close')) { $('#mz-radar-overlay').fadeOut(200); } }); } function injectButton() { const url = window.location.href; const isAllowedPage = url.includes('p=players') || url.includes('p=shortlist'); const isTrainingPage = url.includes('p=training_report') || url.includes('p=training_camp') || url.includes('p=training_field') || url.includes('p=training'); if (!isAllowedPage || isTrainingPage) return; const $fireIcons = $('.fire_player'); $fireIcons.each(function () { const $fireEl = $(this); const $parentRow = $fireEl.closest('div, td'); if ($parentRow.find('.mz-radar-chart-btn').length > 0) return; const $btn = $('<div class="mz-radar-chart-btn" title="Örümcek Grafiği (Radar Chart)" style="display:inline-flex; width: 24px; height: 24px; justify-content: center; align-items: center; margin-left: 6px; vertical-align: middle; cursor: pointer; border-radius: 4px; background: linear-gradient(135deg, #111e25, #2980b9); box-shadow: 0 1px 4px rgba(0,0,0,0.4); font-size: 14px; text-decoration:none;">🕸️</div>'); $btn.on('click', function (e) { e.preventDefault(); let $pContainer = $fireEl.closest('.playerContainer, #playerInfoWindow, .player_container, .profile-container'); if ($pContainer.length === 0) $pContainer = $fireEl.closest('table, .box_dark, div[id]').first(); if ($pContainer.length === 0) $pContainer = $('body'); showRadarChart($pContainer); }); $fireEl.after($btn); }); } function showRadarChart($container) { // Tüm olası isim class'larını ve linkleri tarayıp en iyi sonucu alıyoruz let $titleEl = $container.find('.player_name, .profile-player-info h2, h2.v-profile-player-name, .header-player-name, a.player_name_link, strong.player_name, a[href*="p=players&pid="]').first().clone(); // Eğer h2 içinden alıyorsak, buton veya istatistik label'larını temizleyelim $titleEl.find('*').each(function () { const txt = $(this).text().trim().toLowerCase(); // "İSTATİSTİKLER", "STATISTICS", "PAYLAŞ", "SHARE" vb. temizle if (txt.includes('istatistik') || txt.includes('statistic') || txt.includes('payla') || txt.includes('share') || txt.includes('edit') || txt.includes('düzenle') || $(this).is('button')) { $(this).remove(); } }); // Pozisyonu ayırmaya çalışalım let posInfo = ""; let $posEl = $titleEl.find('.v-profile-player-position, .player-position, span:contains("(")').first(); if ($posEl.length > 0) { posInfo = $posEl.text().trim(); $posEl.remove(); } let nameOnly = $titleEl.text().trim().replace(/\s+/g, ' '); // Ekstra Temizlik: ManagerZone sisteminin veya diğer scriptlerin isim yanına eklediği metinleri (buton etiketleri vb.) temizliyoruz. // i ve İ karakterleri ve olası boşluklar için daha sağlam bir regex. nameOnly = nameOnly.replace(/\s*([iİ]STAT[iİ]ST[iİ]KLER|STATISTICS|PAYLA[sş]|SHARE|D[uü]ZENLE|EDIT)\s*/gi, ' ').trim(); // Eğer posInfo çekilemediyse ama isimde parantez varsa manuel ayır if (!posInfo && nameOnly.includes('(')) { const parts = nameOnly.split('('); nameOnly = parts[0].trim(); posInfo = '(' + parts[1].trim(); } if (!nameOnly || nameOnly === "") nameOnly = lang === 'tr' ? "OYUNCU" : "PLAYER"; // Renkli başlık (Grafik renklerine uyumlu: Mavi ve Kırmızı) const coloredTitle = `<span style="color: #0fbcf9;">${nameOnly}</span> ${posInfo ? '<span style="color: #ff3f34; margin-left:8px;">' + posInfo + '</span>' : ''}`; const skillsData = []; const categories = []; // Skill tablosunu bul const $skillTable = $container.find('table.player_skills, .player_skills_table').first(); const $rows = $skillTable.find('tr'); // ManagerZone beceri tablosu her dilde aynı sırayı takip eder (Hız, Dayanıklılık...). // Dil bağımsız olması için satır indeksini kullanıyoruz. // Ancak bazı tablolarda başlık satırı olabildiği için ilk gerçek beceri satırını bulmalıyız. let startIdx = -1; $rows.each(function (i) { const rowText = $(this).text(); // Eğer satırda parantez içinde sayı veya skillball resmi varsa bu bir beceri satırıdır. if (startIdx === -1 && (rowText.match(/\(\d+\)/) || $(this).find('img[src*="skillball"], .mz-processed-ball, .mz-custom-skill-container').length > 0)) { startIdx = i; } }); if (startIdx === -1) startIdx = 0; // Bulunamazsa baştan başla $rows.each(function (i) { if (i < startIdx) return; const idx = i - startIdx; if (idx >= skillOrder.length) return; // Sadece ilk 11 beceriyi al (Tecrübe/Form hariç) const $row = $(this); const matchedSkill = skillOrder[idx]; let val = 0; const textContent = $row.text().trim(); const mathVal = textContent.match(/\((\d+)\)/); if (mathVal) { val = parseInt(mathVal[1], 10); } else { const ballCount = $row.find('img[src*="skillball"], .mz-processed-ball img, .mz-custom-skill-container img').length; val = ballCount; if (val === 0) { const alt = $row.find('img.skill').attr('alt') || ""; if (alt.includes(':')) val = parseInt(alt.split(':')[1]) || 0; } } let pctValue = 0; let pctMatch = textContent.match(/(-?\d+(?:\.\d+)?)%/); if (!pctMatch) { pctMatch = textContent.match(/(-?\d+(?:,\d+)?)%/); if (pctMatch) pctMatch[1] = pctMatch[1].replace(',', '.'); } if (pctMatch) { pctValue = parseFloat(pctMatch[1]); } if (!isNaN(val)) { // Pozitif gelişimde: (Top Sayısı * 10) + (% / 10) -> Örn: 6 Top %50 = 65.0 // Negatif gerilemede: ((Top Sayısı + 1) * 10) + (% / 10) -> Örn: 6 Top -0.6% = 69.94 let finalScore = (pctValue >= 0) ? (val * 10 + pctValue / 10) : ((val + 1) * 10 + pctValue / 10); finalScore = Math.min(100, parseFloat(finalScore.toFixed(1))); let isMaxed = false; if ($row.find('.maxed, font[color="red"], font[color="#FF0000"], font[color="#ff0000"]').length > 0) { isMaxed = true; } else { $row.find('span, font, td').each(function () { const $el = $(this); const c = $el.css('color'); // FIX: Ignore red if it comes from our scout coloring (gm_s1) if ((c === 'rgb(255, 0, 0)' || c === 'red') && !$el.hasClass('gm_s1') && !$el.closest('td').hasClass('gm_s1')) { isMaxed = true; return false; } }); if (!isMaxed && $row.find('img[src*="R0lGODlhDAAKAJEDAP////8AAMyZmf"]').length > 0) { isMaxed = true; } } skillsData.push({ id: matchedSkill.id, value: finalScore, label: matchedSkill[lang], isMaxed: isMaxed }); } }); // Map to the correct order defined in skillOrder const finalSeries = []; skillOrder.forEach(so => { const found = skillsData.find(d => d.id === so.id); if (found) { finalSeries.push({ y: found.value, marker: { fillColor: found.isMaxed ? '#ff3f34' : '#0fbcf9' // Maxed: Kırmızı, Değilse PES Mavisi }, dataLabels: { color: found.isMaxed ? '#ff3f34' : '#0fbcf9' // Harfler için de aynı kalıp } }); } else { finalSeries.push(0); } categories.push(so[lang]); }); const hc = unsafeWindow.Highcharts || window.Highcharts; if (!hc) { alert("Highcharts yüklenemedi, lütfen sayfayı yenileyin."); return; } const renderChart = () => { $('#mz-radar-modal-title').html(coloredTitle); $('#mz-radar-overlay').css('display', 'flex').hide().fadeIn(200); hc.chart('mz-radar-container', { chart: { polar: true, type: 'area', backgroundColor: 'transparent', style: { fontFamily: "'Segoe UI', Roboto, Helvetica, Arial, sans-serif" } }, title: { text: null }, pane: { size: '80%', background: { backgroundColor: 'transparent', borderWidth: 0 } }, xAxis: { categories: categories, tickmarkPlacement: 'on', lineWidth: 0, gridLineWidth: 1, gridLineColor: 'rgba(255, 255, 255, 0.2)', // Spoke lines labels: { style: { color: '#ecf0f1', fontSize: '11px', fontWeight: 'bold', textOutline: '1px solid #000' } } }, yAxis: { gridLineInterpolation: 'circle', lineWidth: 0, min: 0, max: 100, tickInterval: 20, gridLineColor: 'rgba(255, 255, 255, 0)', // Hidden inner grids labels: { enabled: false }, plotBands: [ { from: 0, to: 20, color: 'rgba(30, 60, 90, 0.9)' }, { from: 20, to: 40, color: 'rgba(40, 80, 115, 0.9)' }, { from: 40, to: 60, color: 'rgba(50, 100, 140, 0.9)' }, { from: 60, to: 80, color: 'rgba(60, 120, 165, 0.9)' }, { from: 80, to: 100, color: 'rgba(70, 140, 190, 0.9)' } ] }, tooltip: { enabled: false // Hover durumunda açılan bilgi penceresi devre dışı bırakıldı }, legend: { enabled: false }, series: [{ name: 'Yetenek Seviyesi', data: finalSeries, pointPlacement: 'on', color: '#e74c3c', // PES Red outer line fillColor: 'rgba(52, 152, 219, 0.8)', // PES Solid blue fill lineWidth: 3, marker: { enabled: true, radius: 4, symbol: 'circle', lineColor: '#ffffff', // Noktanın etrafındaki çerçeve lineWidth: 1 }, dataLabels: { enabled: true, format: '{point.y}', // Puanı göster: 61.1 style: { fontSize: '11px', fontWeight: 'bold', textOutline: '2px #111e25' // Koyu arkaplanla parlak tezat. }, allowOverlap: true, crop: false, // Kritik: Kenarlara taşan yazıları (Örn. 100) kesme! overflow: 'allow', // Kritik: Taşan yazılara izin ver! zIndex: 10, // Hız vs gibi yazıların her zaman üstünde kalması için verticalAlign: 'middle', x: 16, // Noktanın net olarak sağ tarafına kaydır y: 2 } }], credits: { enabled: false }, plotOptions: { area: { animation: { duration: 1000 } } } }); }; // Dinamik olarak highcharts-more kütüphanesini sayfaya göm if (hc.seriesTypes && hc.seriesTypes.polygon) { renderChart(); // Zaten yüklü } else { if ($('#mz-hc-more-script').length === 0) { const script = document.createElement('script'); script.id = 'mz-hc-more-script'; const ver = hc.version || '10.3.3'; script.src = `https://code.highcharts.com/${ver}/highcharts-more.js`; script.onload = renderChart; document.head.appendChild(script); } else { setTimeout(renderChart, 500); // Yüklenmek üzere } } } // Run on load and dynamic changes injectButton(); // Dinamik yüklemeler için setInterval (JANK önleyici) setInterval(injectButton, 1500); } // Initialize the main modules and wait for DOM $(document).ready(function () { initializeQuickAccessMenu(); // HIZLI ERİŞİM (Yıldız Butonu) // owned by legacy-player-advanced initializeSquadDashboardAndPositions(); // Start Dashboard & Positions module // owned by legacy-player-advanced // Maç Sonuç R Butonu + XML Taktik Kopyalama initializeScoutTrainingPlan(); // Scout Bazlı Antrenman Planı Önerisi }); // --- NEW FEATURE 1 & 2: SQUAD DASHBOARD & AI POSITION SUGGESTER --- function initializeSquadDashboardAndPositions() { // Sadece KADRO LİSTESİ ve OYUNCU PROFİL sayfalarında çalış const href = window.location.href; if (href.indexOf('p=players') === -1 && href.indexOf('sub=team_players') === -1 && href.indexOf('sub=scout_report') === -1 && href.indexOf('p=transfer') === -1 && href.indexOf('p=shortlist') === -1) { return; } // --- Dil Desteği (UI için: Türkçe ise tr, değilse her zaman en) --- const i18n = { tr: { scanning: "[MZone Dashboard] Tarama Başladı...", foundCount: "[MZone Dashboard] Bulunan isim/link sayısı:", processedCount: "[MZone Dashboard] İşlenebilen oyuncu bloğu sayısı:", salary: "Maaş", value: "Değer", topSalary: "En Yüksek Maaş", avgAge: "Ort. Yaş", totalSkills: "Top. Top", avgSkills: "Ort. Top", playerCount: "Oyuncu Sayısı", totalSalaryLoad: "Toplam Maaş Yükü", totalSquadValue: "Toplam Kadro Değeri", totalBalls: "Toplam Top", avgBalls: "Ortalama Top", posGK: "Kaleci (KL)", posCB_FB: "Bek (SĞB/SLB)", posCB: "Stoper (STP)", posPivot: "Pivot (PVT)", posST: "Santrafor (SF)", posWinger: "Kanat (KNT)", posMid: "Orta Saha (OS)", posRotation: "Rotasyon", posAnc: "Ön Libero (ÖLB)", speed: "Hız", stamina: "Dayanıklılık", intelligence: "Oyun Zekası", passing: "Paslaşma", shooting: "Şut Çekme", heading: "Kafa Vuruşu", keeping: "Kalecilik", control: "Top Kontrolü", tackling: "Top Çalma", crossing: "Orta Yapma", setplays: "Duran Top", scTitle: "Mevki Puanları (SC)", }, en: { scanning: "[MZone Dashboard] Scanning Started...", foundCount: "[MZone Dashboard] Names/links found:", processedCount: "[MZone Dashboard] Processable player blocks found:", salary: "Salary", value: "Value", topSalary: "Top Salary", avgAge: "Avg. Age", totalSkills: "Total Balls", avgSkills: "Avg. Balls", playerCount: "Player Count", totalSalaryLoad: "Total Salary Load", totalSquadValue: "Total Squad Value", totalBalls: "Total Balls", avgBalls: "Average Balls", posGK: "Goalkeeper (GK)", posCB_FB: "Fullback (RB/LB)", posCB: "Center Back (CB)", posPivot: "Pivot (PVT)", posST: "Striker (ST)", posWinger: "Winger (WNG)", posMid: "Midfielder (MID)", posRotation: "Rotation", posAnc: "Anchor (ANC)", speed: "Speed", stamina: "Stamina", intelligence: "Play Intelligence", passing: "Passing", shooting: "Shooting", heading: "Heading", keeping: "Keeping", control: "Ball Control", tackling: "Tackling", crossing: "Crossing", setplays: "Set Plays", scTitle: "SC Scores", } }; const detectedLang = document.querySelector('meta[name="language"]')?.getAttribute('content') || 'en'; const lang = (detectedLang === 'tr') ? 'tr' : 'en'; const getText = (key) => i18n[lang][key] || i18n['en'][key]; const runAnalysis = () => { let totalSalary = 0; let totalValue = 0; let totalAge = 0; let playerCount = 0; let totalBalls = 0; let $playerRowsArray = []; // Ana oyuncu listesindeki isimleri hedef al const $names = $('#contentDiv, #ajaxContainer').find('.player_name_link, a.player_name_link, strong.player_name, h2.v-profile-player-name, a[href*="?p=players&pid="]').filter(function () { if ($(this).closest('.powerbox, .ui-dialog, .player_tc_package_information').length > 0) return false; return $(this).text().trim().length > 2; }); // PERFORMANS FIX: Eğer ekrandaki oyuncu sayısı, ilk oyuncu PID'si veya antrenman verisi sayısı değişmediyse taramayı İPTAL ET. const firstPid = ($names.first().attr('href') || "").match(/pid=(\d+)/)?.[1] || ""; const exactCount = $('#contentDiv, #ajaxContainer').find('.skill_exact2').length; const currentHash = $names.length + "_" + firstPid + "_" + exactCount; if (window._lastDashboardHash === currentHash) { return; } window._lastDashboardHash = currentHash; console.log(getText('scanning')); console.log(getText('foundCount'), $names.length); $names.each(function () { // Daha esnek konteynır tespiti (Borsa ve Takip listesi için) let $p = $(this).closest('.playerContainer, .player_container, .box_dark, .profile-player-info, #playerInfoWindow'); if ($p.length === 0) { $p = $(this).closest('table.player_skills').parent(); } if ($p.length === 0) { $p = $(this).closest('tr').closest('table'); } // Dil-bağımsız oyuncu bloğu tespiti: Beceri tablosu veya top ikonları var mı? const $skillTable = $p.find('table.player_skills, .player_skills_table'); const isPlayerBlock = $skillTable.length > 0 || $p.find('img[src*="skillball"]').length > 0; // --- Hassas Hesaplama Bekleme Mantığı --- // Eğer oyuncunun beceri tablosu varsa ama antrenman modülü henüz bitirmemişse (ve önbellekte yoksa), bekle. if ($skillTable.length > 0 && !$p.hasClass('mz-training-finished')) { // console.log(`[MZone SC] ID ${$(this).text().trim()} için antrenman verileri bekleniyor...`); return; } if (isPlayerBlock) { $playerRowsArray.push($p[0]); } }); $playerRowsArray = Array.from(new Set($playerRowsArray)); const $playerRows = $($playerRowsArray); console.log(getText('processedCount'), $playerRows.length); // Oyuncu satırlarını işle $playerRows.each(function () { const $rowRow = $(this); const skills = { spd: 0, stm: 0, pI: 0, pas: 0, sht: 0, hea: 0, kp: 0, tc: 0, tck: 0, crs: 0, set: 0, exp: 0, frm: 0 }; const $skillTable = $rowRow.find('table.player_skills, .player_skills_table').first(); if ($skillTable.length > 0) { $skillTable.find('tr').each(function () { const $tr = $(this); const rowText = $tr.text().trim(); const skillNameRaw = $tr.find('td:first').text().trim(); let val = 0; const $img = $tr.find('img.skill, img[src*="level_"]'); if ($img.length > 0) { const alt = $img.attr('alt') || ''; const match = alt.match(/(\d+)/); if (match) val = parseInt(match[0], 10); } if (val === 0) { val = $tr.find('img[src*="skillball"], .mz-processed-ball img, .mz-custom-skill-container img').length; if (val === 0) { const mathVal = rowText.match(/\((\d+)\)/); if (mathVal) val = parseInt(mathVal[1], 10); } } // --- Hassas Hesaplama (Antrenman/Gerileme) Entegrasyonu --- let $exact = $tr.find('.skill_exact2'); let exactText = $exact.text().trim(); // YEDEK: Eğer özel class'ta bulamazsa tüm satırda % ara (Görseldeki durum için) if (!exactText.includes('%')) { exactText = rowText; } const percentMatch = exactText.includes('%') ? exactText.match(/([+-]?\d+[\.,]?\d*)/) : null; if (percentMatch && val > 0) { const p = parseFloat(percentMatch[1].replace(',', '.')); if (!isNaN(p) && p !== 0) { if (p < 0) { if (val < 10) val = val + 1 + (p / 100); else val = val + (p / 100); } else { val = val + (p / 100); } } } if (val % 1 !== 0) { console.log(`[MZone SC Precise] ${skillNameRaw} -> Küsuratlı Değer: ${val.toFixed(3)}`); } if (val > 0) totalBalls += val; // Debug: Her satırı ayrı ayrı logla if (skillNameRaw.length > 0) { console.log(`[MZone SC Debug] Row: "${skillNameRaw}", Value: ${val}`); } // Case-insensitive and character-normalized matching const clean = (s) => s.toLowerCase().replace(/\s/g, '').replace(/ı/g, 'i').replace(/ş/g, 's').replace(/ç/g, 'c').replace(/ö/g, 'o').replace(/ü/g, 'u').replace(/ğ/g, 'g'); const cName = clean(skillNameRaw); // Antrenman satırlarının gerçek yetenekleri ezmesini engelle const isAnt = cName.includes('ant'); if (!isAnt) { if ((cName.includes('hiz') || cName.includes('hz')) && !skills.spd) skills.spd = val; else if ((cName.includes('dayanik') || cName.includes('day')) && !skills.stm) skills.stm = val; else if ((cName.includes('zek') || cName.includes('oz')) && !skills.pI) skills.pI = val; else if (cName.includes('pas') && !skills.pas) skills.pas = val; else if (cName.includes('sut') && !skills.sht) skills.sht = val; else if (cName.includes('kafa') && !skills.hea) skills.hea = val; else if (cName.includes('kaleci') && !skills.kp) skills.kp = val; else if (cName.includes('kontrol') && !skills.tc) skills.tc = val; else if (cName.includes('calma') && !skills.tck) skills.tck = val; else if (cName.includes('orta') && !skills.crs) skills.crs = val; else if (cName.includes('duran') && !skills.set) skills.set = val; else if (cName.includes('tecrube') && !skills.exp) skills.exp = val; else if (cName.includes('form') && !skills.frm) skills.frm = val; } }); // Log extracted skills for debugging const playerIdSpan = $rowRow.find('.player_id_span').first(); const playerId = playerIdSpan.length > 0 ? playerIdSpan.text().trim() : "Unknown"; console.log(`[MZone SC] Player ID: ${playerId}, Extracted Skills:`, JSON.stringify(skills)); // --- Dynamic Position Score (SC) Calculation (Based on MZMgr logic) --- const calculateSC = (s) => { const weights = { GK: { spd: 9, stm: 9, pI: 9, pas: 2, sht: 0, hea: 0, kp: 55, tc: 9, tck: 0, crs: 2, set: 0, exp: 5 }, DF: { spd: 25, stm: 16, pI: 7, pas: 2, sht: 0, hea: 0, kp: 0, tc: 8, tck: 30, crs: 7, set: 0, exp: 5 }, ANC: { spd: 25, stm: 18, pI: 5, pas: 5, sht: 0, hea: 2, kp: 0, tc: 10, tck: 25, crs: 5, set: 0, exp: 5 }, MF: { spd: 15, stm: 15, pI: 10, pas: 15, sht: 0, hea: 0, kp: 0, tc: 12, tck: 20, crs: 8, set: 0, exp: 5 }, WG: { spd: 25, stm: 20, pI: 6, pas: 4, sht: 5, hea: 0, kp: 0, tc: 15, tck: 5, crs: 15, set: 0, exp: 5 }, FW: { spd: 23, stm: 15, pI: 5, pas: 4, sht: 28, hea: 3, kp: 0, tc: 15, tck: 2, crs: 0, set: 0, exp: 5 } }; const results = []; for (const [pos, w] of Object.entries(weights)) { let score = 0; for (const [skill, weight] of Object.entries(w)) { score += (s[skill] || 0) * weight; } let finalScore = score / 100; results.push({ pos, score: finalScore.toFixed(2) }); } return results.sort((a, b) => b.score - a.score); }; const scScores = calculateSC(skills); console.log(`[MZone SC] ID ${playerId} Results:`, JSON.stringify(scScores)); // Global check function setup (outside loop ideally, but this works for testing) window.checkPlayerSC = function(id) { console.log("Looking for player ID:", id); const $p = $(`.player_id_span:contains('${id}')`).closest('.playerContainer, .player_block, tr'); if($p.length === 0) return "Player not found in DOM"; return "Check console for log starting with [MZone SC] ID " + id; }; const bestPos = scScores[0]; let position = ""; let badgeColor = "#555"; const langMeta = document.querySelector('meta[name="language"]'); const isTr = langMeta && langMeta.getAttribute('content') === 'tr'; if (bestPos.pos === "GK") { position = isTr ? "Kaleci" : "GK"; badgeColor = "#e67e22"; } else if (bestPos.pos === "DF") { position = isTr ? "Stoper" : "CB"; badgeColor = "#27ae60"; } else if (bestPos.pos === "ANC") { position = isTr ? "Ön Libero" : "ANC"; badgeColor = "#16a085"; } else if (bestPos.pos === "MF") { position = isTr ? "Orta Saha" : "MF"; badgeColor = "#2980b9"; } else if (bestPos.pos === "WG") { position = isTr ? "Kanat" : "Winger"; badgeColor = "#8e44ad"; } else if (bestPos.pos === "FW") { position = isTr ? "Santrafor" : "Striker"; badgeColor = "#e74c3c"; } const getPosName = (p) => { if (p === "GK") return isTr ? "Kaleci" : "GK"; if (p === "DF") return isTr ? "Stoper" : "CB"; if (p === "ANC") return isTr ? "Ön Libero" : "ANC"; if (p === "MF") return isTr ? "Orta Saha" : "MF"; if (p === "WG") return isTr ? "Kanat" : "Winger"; if (p === "FW") return isTr ? "Santrafor" : "Striker"; return p; }; const tooltipContent = scScores.map(s => { const color = s.score >= 8 ? "#2ecc71" : s.score >= 6 ? "#f1c40f" : "#e74c3c"; return `<div style='display:flex; justify-content:space-between; gap:10px; border-bottom:1px solid rgba(255,255,255,0.1); padding:2px 0;'><span>${getPosName(s.pos)}:</span> <b style='color:${color}'>${s.score}</b></div>`; }).join(''); let dataAttrs = `data-mz-pos="${bestPos.pos}" data-mz-score="${bestPos.score}"`; scScores.forEach(s => { dataAttrs += ` data-mz-score-${s.pos.toLowerCase()}="${s.score}"`; }); const $nameContainer = $rowRow.find('.player_name, .profile-player-info h2, h2.v-profile-player-name, a.player_name_link, strong.player_name, a[href*="p=players&pid="]').first(); const $effContainer = $rowRow.find('.genel-verim-container'); const isTransfer = window.location.href.includes('p=transfer') || window.location.href.includes('p=shortlist'); // Eski rozeti sil $rowRow.find('.mz-ai-position-badge').remove(); if (position !== "") { // Mevcut sıralama seçimini kontrol et const currentSort = $('#mz-sort-positions-select').val() || "ALL"; let displayPos = position; let displayScore = bestPos.score; let displayBg = badgeColor; if (currentSort !== "ALL") { const nameMap = isTr ? { "GK": "Kaleci", "DF": "Stoper", "ANC": "Ön Libero", "MF": "Orta Saha", "WG": "Kanat", "FW": "Santrafor" } : { "GK": "GK", "DF": "CB", "ANC": "ANC", "MF": "MF", "WG": "Winger", "FW": "Striker" }; const colorMap = { "GK": "#e67e22", "DF": "#27ae60", "ANC": "#16a085", "MF": "#2980b9", "WG": "#8e44ad", "FW": "#e74c3c" }; const specific = scScores.find(s => s.pos === currentSort); if (specific) { displayPos = nameMap[currentSort]; displayScore = specific.score; displayBg = colorMap[currentSort]; } } const marginStyle = (isTransfer && $effContainer.length > 0) ? "margin-right:8px;" : "margin-left:8px;"; const badgeHtml = `<span class="mz-ai-position-badge" title="<div style='font-family:sans-serif; min-width:140px;'><div style='font-weight:bold; border-bottom:1px solid #fff; margin-bottom:5px; padding-bottom:2px;'>${getText('scTitle')}</div>${tooltipContent}</div>" ${dataAttrs} data-mz-orig-text="${position}: ${bestPos.score}" data-mz-orig-bg="${badgeColor}" style="background-color:${displayBg}; color:#fff; font-size:10px; font-weight:bold; padding:2px 6px; border-radius:4px; ${marginStyle} display:inline-flex; align-items:center; height:18px; line-height:1; text-shadow:0 1px 1px rgba(0,0,0,0.5); box-shadow:0 1px 3px rgba(0,0,0,0.3); letter-spacing:0.5px; border:1px solid rgba(255,255,255,0.2); cursor:help;">${displayPos}: ${displayScore}</span>`; if (isTransfer && $effContainer.length > 0) { $effContainer.prepend(badgeHtml); } else { $nameContainer.append(badgeHtml); } $nameContainer.find('.mz-ai-position-badge').each(function() { $(this).attr('data-mz-tooltip', 'true'); }); } } // --- Dashboard Verilerini Topla (Genişletilmiş Dil Desteği) --- const rowHtml = $rowRow.html(); const rowText = $rowRow.text().replace(/\xA0/g, ' ') || ""; // Yaş/Age/Edad/Âge/Alter... tespiti const ageMatch = rowHtml.match(/(?:Yaş|Age|Edad|Âge|Alter|Età|Idade|Leeftijd|Ålder|Alder|Prowat|Roky):\s*(\d{1,2})/i) || rowText.match(/(?:Yaş|Age|Edad|Âge|Alter|Età|Idade|Leeftijd|Ålder|Alder|Prowat|Roky):\s*(\d{1,2})/i); if (ageMatch) { totalAge += parseInt(ageMatch[1], 10); } // Maaş/Salary/Salario/Wage/Salaire... tespiti const salaryMatch = rowHtml.match(/(?:Maaş|Salary|Wage|Salario|Salaire|Gehalt|Stipendio|Salário|Salaris|Lön|Løn|Palkka|Wynagrodzenie|Plat):\s*([\d\s\.,]+)\s*(?:EUR|USD|TRY|£|€|kr|TL|zł|Kč)/i) || rowText.match(/(?:Maaş|Salary|Wage|Salario|Salaire|Gehalt|Stipendio|Salário|Salaris|Lön|Løn|Palkka|Wynagrodzenie|Plat):\s*([\d\s\.,]+)\s*(?:EUR|USD|TRY|£|€|kr|TL|zł|Kč)/i); if (salaryMatch) { totalSalary += parseInt(salaryMatch[1].replace(/[\s\.,]/g, ''), 10); } // Değer/Value/Valor/Valeur/Wert... tespiti const valueMatch = rowHtml.match(/(?:Değer|Value|Valor|Valeur|Wert|Valore|Waarde|Värde|Værdi|Arvo|Wartość|Cena):\s*([\d\s\.,]+)\s*(?:EUR|USD|TRY|£|€|kr|TL|zł|Kč)/i) || rowText.match(/(?:Değer|Value|Valor|Valeur|Wert|Valore|Waarde|Värde|Værdi|Arvo|Wartość|Cena):\s*([\d\s\.,]+)\s*(?:EUR|USD|TRY|£|€|kr|TL|zł|Kč)/i); if (valueMatch) { totalValue += parseInt(valueMatch[1].replace(/[\s\.,]/g, ''), 10); } playerCount++; }); if (playerCount === 0) return; const formatCurrency = (num) => new Intl.NumberFormat(lang === 'tr' ? 'tr-TR' : 'en-US').format(num) + ' €'; let avgAge = (totalAge / playerCount).toFixed(1); let avgBalls = (totalBalls / playerCount).toFixed(1); if (playerCount > 1 && !window.location.href.includes('p=transfer') && !window.location.href.includes('p=shortlist')) { if ($('#mz-squad-dynamic-dashboard').length === 0) { const dashboardHtml = ` <div id="mz-squad-dynamic-dashboard" style="display: flex; justify-content: space-around; align-items: center; background: linear-gradient(135deg, #111e25, #1e3c50); padding: 12px 20px; border-radius: 8px; border-bottom: 3px solid #f39c12; margin-bottom: 15px; box-shadow: 0 4px 10px rgba(0,0,0,0.3); color: #ecf0f1; font-family: 'Segoe UI', Tahoma, sans-serif; pointer-events: auto;"> <div style="text-align: center;"> <div style="font-size: 10px; text-transform: uppercase; color: #bdc3c7; letter-spacing: 1px; margin-bottom: 2px;">${getText('playerCount')}</div> <div style="font-size: 18px; font-weight: bold; color: #fff;">${playerCount} <i class="fa fa-users" style="font-size:14px; color:#3498db;"></i></div> </div> <div style="width: 1px; height: 30px; background: rgba(255,255,255,0.1);"></div> <div style="text-align: center;"> <div style="font-size: 10px; text-transform: uppercase; color: #bdc3c7; letter-spacing: 1px; margin-bottom: 2px;">${getText('avgAge')}</div> <div style="font-size: 18px; font-weight: bold; color: #fff;">${avgAge} <i class="fa fa-birthday-cake" style="font-size:14px; color:#e74c3c;"></i></div> </div> <div style="width: 1px; height: 30px; background: rgba(255,255,255,0.1);"></div> <div style="text-align: center;"> <div style="font-size: 10px; text-transform: uppercase; color: #bdc3c7; letter-spacing: 1px; margin-bottom: 2px;">${getText('totalSalaryLoad')}</div> <div style="font-size: 18px; font-weight: bold; color: #fff;">${formatCurrency(totalSalary)} <i class="fa fa-money" style="font-size:14px; color:#2ecc71;"></i></div> </div> <div style="width: 1px; height: 30px; background: rgba(255,255,255,0.1);"></div> <div style="text-align: center;"> <div style="font-size: 10px; text-transform: uppercase; color: #bdc3c7; letter-spacing: 1px; margin-bottom: 2px;">${getText('totalSquadValue')}</div> <div style="font-size: 18px; font-weight: bold; color: #f1c40f;">${formatCurrency(totalValue)} <i class="fa fa-bank" style="font-size:14px;"></i></div> </div> <div style="width: 1px; height: 30px; background: rgba(255,255,255,0.1);"></div> <div style="text-align: center;"> <div style="font-size: 10px; text-transform: uppercase; color: #bdc3c7; letter-spacing: 1px; margin-bottom: 2px;">${getText('avgBalls')}</div> <div style="font-size: 18px; font-weight: bold; color: #3498db;">${avgBalls} <i class="fa fa-futbol-o" style="font-size:14px;"></i></div> </div> </div>`; const $filterWrapper = $('#player-filters-wrapper'); const $playerFilters = $('#player_filters'); if ($filterWrapper.length > 0) { $filterWrapper.after(dashboardHtml); } else if ($playerFilters.length > 0) { $playerFilters.after(dashboardHtml); } else { $('#contentDiv').prepend(dashboardHtml); } } else { const $dash = $('#mz-squad-dynamic-dashboard'); const sections = $dash.find('> div[style*="text-align: center"]'); if (sections.length >= 4) { $(sections[0]).find('div:first-child').text(getText('playerCount')); $(sections[0]).find('div:last-child').html(`${playerCount} <i class="fa fa-users" style="font-size:14px; color:#3498db;"></i>`); $(sections[1]).find('div:first-child').text(getText('avgAge')); $(sections[1]).find('div:last-child').html(`${avgAge} <i class="fa fa-birthday-cake" style="font-size:14px; color:#e74c3c;"></i>`); $(sections[2]).find('div:first-child').text(getText('totalSalaryLoad')); $(sections[2]).find('div:last-child').html(`${formatCurrency(totalSalary)} <i class="fa fa-money" style="font-size:14px; color:#2ecc71;"></i>`); $(sections[3]).find('div:first-child').text(getText('totalSquadValue')); $(sections[3]).find('div:last-child').html(`${formatCurrency(totalValue)} <i class="fa fa-bank" style="font-size:14px;"></i>`); if (sections.length >= 5) { $(sections[4]).find('div:first-child').text(getText('avgBalls')); $(sections[4]).find('div:last-child').html(`${avgBalls} <i class="fa fa-futbol-o" style="font-size:14px;"></i>`); } } } } else { $('#mz-squad-dynamic-dashboard').remove(); } }; // Pozisyon rozetlerine tıklanınca üstteki linkin tetiklenmesini engelle $(document).on('click', '.mz-ai-position-badge', function (e) { e.preventDefault(); e.stopPropagation(); }); setTimeout(runAnalysis, 1500); window._triggerDashboardUpdate = runAnalysis; // PERFORMANS FIX: MZone Dashboard'un kendi kendine sonsuz döngüye girmesini ve kaydırma anında sayfayı dondurmasını (JANK) tamamen önler. // MutationObserver yerine her 1.5 saniyede bir sessizce çalıştırılır. setInterval(runAnalysis, 1500); } /**************************************************************************************** * * * BÖLÜM 24: MAÇ SONUÇ "R" BUTONU + XML TAKTİK KOPYALAMA (van.mz portlanmış) * * * ****************************************************************************************/ function initializeMatchResultButton() { 'use strict'; const $ = jQuery; // Sadece maç detay sayfasında çalış const href = window.location.href; if (href.indexOf('mid=') === -1 && href.indexOf('p=match') === -1) return; // Koordinat dönüşüm formülleri (van.mz'den) function statsToPos_X(i, isLocal) { return isLocal ? Math.round(-0.2558004620 * i + 199.8228530689) : Math.round(0.2555000556 * i + 8.3741302936); } function statsToPos_Y(i, isLocal) { return isLocal ? Math.round(-0.3073207154 * i + 315.9278777381) : Math.round(0.3070644902 * i + 9.2794889414); } // Depo: maç XML verisi (matchId → XML document) const mzMatchXmlCache = {}; function getMatchXML(matchId, callback) { if (mzMatchXmlCache[matchId]) { return callback(mzMatchXmlCache[matchId]); } const statsUrl = 'https://www.managerzone.com/matchviewer/getMatchFiles.php?type=stats&mid=' + matchId + '&sport=soccer'; const checkUrl = 'https://www.managerzone.com/ajax.php?p=matchViewer&sub=check-match&type=2d&mid=' + matchId + '&sport=soccer'; const errMsgNoData = getText('errMsgNoData'); // Stats XML'i doğrudan çek function fetchStats(onSuccess, onFail) { GM_xmlhttpRequest({ method: 'GET', url: statsUrl, onload: function (resp) { if (resp.status === 200 && resp.responseText && resp.responseText.trim().charAt(0) === '<') { try { const data = $.parseXML(resp.responseText); const teams = data.documentElement.getElementsByTagName('Team'); if (teams && teams.length >= 2) { mzMatchXmlCache[matchId] = data; onSuccess(data); return; } } catch (e) { /* XML parse hatası, fallback */ } } onFail(); }, onerror: function () { onFail(); } }); } // check-match ile MZ sunucusunu tetikle, ardından stats XML al // Sayfa jQuery kullanılıyor — oturum cookie + XMLHttpRequest başlığı otomatik eklenir function checkAndRetry(tryCount) { if (tryCount > 5) { alert(errMsgNoData); return; } $.getJSON(checkUrl) .done(function (result) { switch (result.response) { case 'ok': fetchStats(callback, function () { setTimeout(function () { fetchStats(callback, function () { alert(errMsgNoData); }); }, 2000); }); break; case 'queued': setTimeout(function () { checkAndRetry(tryCount + 1); }, 3000); break; case 'walkover': case 'blocked': case 'error': default: alert(lang === 'tr' ? 'Maç verisi hazırlanamadı (durum: ' + (result.response || 'bilinmiyor') + '). ' + 'Bu maç için XML veri üretilemiyor olabilir.' : 'Match data could not be prepared (status: ' + (result.response || 'unknown') + ').'); } }) .fail(function (xhr, status, err) { console.error('[MZone R] check-match hatası:', status, err, xhr.responseText && xhr.responseText.substring(0, 100)); // $.getJSON başarısız → GM_xmlhttpRequest ile fallback dene GM_xmlhttpRequest({ method: 'GET', url: checkUrl, headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }, onload: function (resp) { let result = {}; try { result = JSON.parse(resp.responseText); } catch (e) { } if (result.response === 'ok') { fetchStats(callback, function () { alert(errMsgNoData); }); } else if (result.response === 'queued') { setTimeout(function () { checkAndRetry(tryCount + 1); }, 3000); } else { alert(errMsgNoData); } }, onerror: function () { alert(errMsgNoData); } }); }); } // Önce doğrudan dene, başarısız olursa check-match mekanizmasına geç fetchStats(callback, function () { checkAndRetry(1); }); } function stats2XML(data, ishome) { if (!data) return ''; const teams = data.documentElement.getElementsByTagName('Team'); const team = ishome ? teams[0] : teams[1]; let tmpXML = '<?xml version="1.0" ?>\r\n<SoccerTactics>\r\n' + '\t<Team tactics="' + team.getAttribute('tactic') + '" playstyle="' + team.getAttribute('playstyle') + '" aggression="' + team.getAttribute('aggression') + '" />\r\n' + '\t<Pos pos="goalie" pid="0" x="103" y="315" x1="103" y1="315" x2="103" y2="315" pt="15" fk="15" />\r\n'; const players_xml = data.documentElement.getElementsByTagName('Player'); let slotIndex = 0; for (let i = 0; i < players_xml.length; i++) { const pl = players_xml[i]; const origin = pl.getAttribute('origin'); const teamId = pl.getAttribute('teamId'); if (origin && origin !== '' && origin !== '375,0' && origin !== '375,1000') { const arr = origin.split(','); if (team.getAttribute('id') === teamId) { const x = statsToPos_X(arr[0], ishome); const y = statsToPos_Y(arr[1], ishome); tmpXML += '\t<Pos pos="normal" pid="' + slotIndex + '" x="' + x + '" y="' + y + '" x1="' + x + '" y1="' + y + '" x2="' + x + '" y2="' + y + '" pt="1" fk="1" />\r\n'; slotIndex++; } } } tmpXML += '</SoccerTactics>\r\n'; return tmpXML; } function copyXML(data, ishome, matchId) { const xml = stats2XML(data, ishome); if (!xml) { alert(getText('copyError')); return; } // Extract match info let teamName = "Takım"; let matchScore = "Bilinmiyor"; try { // Maç sayfasındaki DOM'dan isimleri çekmeyi dene const homeNameEl = document.querySelector('.team-left a') || document.querySelector('.team-home a') || document.querySelector('#team_home a'); const awayNameEl = document.querySelector('.team-right a') || document.querySelector('.team-away a') || document.querySelector('#team_away a'); const homeName = homeNameEl ? homeNameEl.textContent.trim() : "Ev Sahibi"; const awayName = awayNameEl ? awayNameEl.textContent.trim() : "Deplasman"; teamName = ishome ? homeName : awayName; // XML'den skorları çek const teams = data.documentElement.getElementsByTagName('Team'); if (teams.length >= 2) { const homeS = teams[0].getElementsByTagName('Statistics')[0]; const awayS = teams[1].getElementsByTagName('Statistics')[0]; if (homeS && awayS) { const g1 = parseInt(homeS.getAttribute('goals'), 10) || 0; const g2 = parseInt(awayS.getAttribute('goals'), 10) || 0; matchScore = `${homeName} ${g1} - ${g2} ${awayName}`; } } } catch (e) { } let rawTactic = ''; let rawPlaystyle = ''; let rawAggression = ''; // Taktik bilgilerini localStorage'a kaydet (taktik sayfasında otomatik doldurma için) try { const teams = data.documentElement.getElementsByTagName('Team'); const team = ishome ? teams[0] : teams[1]; if (team) { rawTactic = team.getAttribute('tactic') || ''; rawPlaystyle = team.getAttribute('playstyle') || ''; rawAggression = team.getAttribute('aggression') || ''; const tacticSettings = { tactics: rawTactic, playstyle: rawPlaystyle, aggression: rawAggression }; localStorage.setItem('mz_autofill_tactic', JSON.stringify(tacticSettings)); } } catch (e) { /* localStorage erişim hatası - görmezden gel */ } // Geçmişi kaydet ve oku let history = []; try { history = JSON.parse(localStorage.getItem('mz_tactic_history') || '[]'); } catch (e) { } if (!matchId) { const matchIdMatch = window.location.href.match(/match_id=(\d+)|mid=(\d+)/); if (matchIdMatch) matchId = matchIdMatch[1] || matchIdMatch[2]; } const newItem = { id: Date.now(), matchScore: matchScore, teamName: teamName, tactic: rawTactic, playstyle: rawPlaystyle, aggression: rawAggression, xml: xml, matchId: matchId || '', isHome: ishome, date: new Date().toLocaleString('tr-TR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) }; // Başa ekle, maksimum 10 tane tut history.unshift(newItem); if (history.length > 10) history.length = 10; try { localStorage.setItem('mz_tactic_history', JSON.stringify(history)); } catch (e) { } const triggerModal = function () { if (typeof window._mzShowTacticHistoryModal === 'function') { window._mzShowTacticHistoryModal(true); } else { alert(getText('xmlCopied')); } }; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(xml).then(triggerModal).catch(function () { // Fallback const ta = document.createElement('textarea'); ta.value = xml; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); triggerModal(); }); } else { const ta = document.createElement('textarea'); ta.value = xml; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); triggerModal(); } } // Canlı maç dahil: taktik zaman çizelgesi + CopyXML linkleri doldur function _processTacticsAndCopyXML(data, homeT, awayT) { $('.mz-gm-timeline').remove(); const events = data.documentElement.getElementsByTagName('Events'); if (events.length) { const tacs = events[0].getElementsByTagName('Tactic'); const timeline = $('table.timeline.hitlist.marker'); if (timeline.length) { let trclass = false; for (let i = tacs.length - 1; i >= 0; i--) { const tac = tacs[i]; const tacInfo = tac.getAttribute('type') + '->' + tac.getAttribute('new_setting'); const min = tac.getAttribute('time').split(':')[0] + "'"; let row; if (tac.getAttribute('teamId') === homeT.getAttribute('id')) { row = "<tr class='mz-gm-timeline " + (trclass ? 'even' : 'odd') + "'>" + "<td align='right' width='50%'><span style='white-space:nowrap'><strong>" + tacInfo + "</strong></span></td>" + "<td align='center' valign='middle' width='40'><strong class='time'>" + min + "</strong></td>" + "<td align='left' width='50%'> </td></tr>"; } else { row = "<tr class='mz-gm-timeline " + (trclass ? 'even' : 'odd') + "'>" + "<td align='right' width='50%'> </td>" + "<td align='center' valign='middle' width='40'><strong class='time'>" + min + "</strong></td>" + "<td align='left' width='50%'><span style='white-space:nowrap'><strong>" + tacInfo + "</strong></span></td></tr>"; } timeline.find('tr:first').before(row); trclass = !trclass; } const homeStart = homeT.getAttribute('tactic') + ' ' + homeT.getAttribute('playstyle') + ' ' + homeT.getAttribute('aggression'); const awayStart = awayT.getAttribute('tactic') + ' ' + awayT.getAttribute('playstyle') + ' ' + awayT.getAttribute('aggression'); const initRow = "<tr class='mz-gm-timeline " + (trclass ? 'even' : 'odd') + "'>" + "<td align='right' width='50%'><span style='white-space:nowrap'><strong>" + homeStart + "</strong>" + "<p><a href='javascript:void(0);' class='mz-copy-xml-btn' data-side='home'>" + getText('copyHome') + "</a></p></span></td>" + "<td align='center' valign='middle' width='40'><strong class='time'>0'</strong></td>" + "<td align='left' width='50%'><span style='white-space:nowrap'><strong>" + awayStart + "</strong>" + "<p><a href='javascript:void(0);' class='mz-copy-xml-btn' data-side='away'>" + getText('copyAway') + "</a></p></span></td></tr>"; timeline.find('tr:first').before(initRow); $(document).off('click', '.mz-copy-xml-btn').on('click', '.mz-copy-xml-btn', function (e) { e.preventDefault(); const matchIdMatch = window.location.href.match(/match_id=(\d+)|mid=(\d+)/); const mid = matchIdMatch ? (matchIdMatch[1] || matchIdMatch[2]) : null; copyXML(data, $(this).data('side') === 'home', mid); }); } } } function showMatchResult(matchId) { getMatchXML(matchId, function (data) { const teams = data.documentElement.getElementsByTagName('Team'); if (!teams || teams.length < 2) return; const homeT = teams[0]; const awayT = teams[1]; const homeS = teams[0].getElementsByTagName('Statistics')[0]; const awayS = teams[1].getElementsByTagName('Statistics')[0]; // Canlı maçlarda Statistics elementi olmayabilir — sadece taktik/XML kısmını doldur if (!homeS || !awayS) { console.warn('[MZone R-Btn] Statistics yok — taktik zaman çizelgesi dolduruluyor (canlı maç).'); _processTacticsAndCopyXML(data, homeT, awayT); return; } // Skor görüntüsünü güncelle const imgs = $('div.scoreboard.shadow img'); if (imgs.length >= 4) { let nocacheUrl = 'nocache-715'; const mth = imgs[0] && imgs[0].src.match(/nocache-\d+/); if (mth) nocacheUrl = mth[0]; const g1 = parseInt(homeS.getAttribute('goals'), 10) || 0; const g2 = parseInt(awayS.getAttribute('goals'), 10) || 0; if (g1 >= 10) { imgs[0].src = nocacheUrl + '/img/score/' + Math.floor(g1 / 10) + '.gif'; imgs[1].src = nocacheUrl + '/img/score/' + (g1 % 10) + '.gif'; } else { imgs[0].src = nocacheUrl + '/img/score/null.gif'; imgs[1].src = nocacheUrl + '/img/score/' + g1 + '.gif'; } if (g2 >= 10) { imgs[3].src = nocacheUrl + '/img/score/' + Math.floor(g2 / 10) + '.gif'; imgs[4].src = nocacheUrl + '/img/score/' + (g2 % 10) + '.gif'; } else { imgs[3].src = nocacheUrl + '/img/score/' + g2 + '.gif'; imgs[4].src = nocacheUrl + '/img/score/null.gif'; } } // İstatistik satırlarını doldur const td = $('div#match-tactic-facts-wrapper div div table.hitlist.statsLite tbody tr td'); if (td.length > 0) { td.eq(1).html(homeS.getAttribute('goals')); td.eq(2).html(awayS.getAttribute('goals')); td.eq(4).html(homeS.getAttribute('injuries')); td.eq(5).html(awayS.getAttribute('injuries')); td.eq(7).html(homeS.getAttribute('yellowCards')); td.eq(8).html(awayS.getAttribute('yellowCards')); td.eq(10).html(homeS.getAttribute('redCards')); td.eq(11).html(awayS.getAttribute('redCards')); td.eq(13).html(homeS.getAttribute('freekicks')); td.eq(14).html(awayS.getAttribute('freekicks')); td.eq(16).html(homeS.getAttribute('penaltyshots')); td.eq(17).html(awayS.getAttribute('penaltyshots')); td.eq(19).html(homeS.getAttribute('corners')); td.eq(20).html(awayS.getAttribute('corners')); td.eq(22).html(homeS.getAttribute('shotsOnGoal')); td.eq(23).html(awayS.getAttribute('shotsOnGoal')); td.eq(25).html((homeS.getAttribute('possession') || '') + '%'); td.eq(26).html((awayS.getAttribute('possession') || '') + '%'); } // Taktik zaman çizelgesi $('.mz-gm-timeline').remove(); const events = data.documentElement.getElementsByTagName('Events'); if (events.length) { const tacs = events[0].getElementsByTagName('Tactic'); const timeline = $('table.timeline.hitlist.marker'); if (timeline.length) { let trclass = false; for (let i = tacs.length - 1; i >= 0; i--) { const tac = tacs[i]; const tacInfo = tac.getAttribute('type') + '->' + tac.getAttribute('new_setting'); const min = tac.getAttribute('time').split(':')[0] + "'"; let row; if (tac.getAttribute('teamId') === homeT.getAttribute('id')) { row = "<tr class='mz-gm-timeline " + (trclass ? 'even' : 'odd') + "'>" + "<td align='right' width='50%'><span style='white-space:nowrap'><strong>" + tacInfo + "</strong></span></td>" + "<td align='center' valign='middle' width='40'><strong class='time'>" + min + "</strong></td>" + "<td align='left' width='50%'> </td></tr>"; } else { row = "<tr class='mz-gm-timeline " + (trclass ? 'even' : 'odd') + "'>" + "<td align='right' width='50%'> </td>" + "<td align='center' valign='middle' width='40'><strong class='time'>" + min + "</strong></td>" + "<td align='left' width='50%'><span style='white-space:nowrap'><strong>" + tacInfo + "</strong></span></td></tr>"; } timeline.find('tr:first').before(row); trclass = !trclass; } // 0. dakika: başlangıç taktikleri + CopyXML linkleri const homeStart = homeT.getAttribute('tactic') + ' ' + homeT.getAttribute('playstyle') + ' ' + homeT.getAttribute('aggression'); const awayStart = awayT.getAttribute('tactic') + ' ' + awayT.getAttribute('playstyle') + ' ' + awayT.getAttribute('aggression'); const initRow = "<tr class='mz-gm-timeline " + (trclass ? 'even' : 'odd') + "'>" + "<td align='right' width='50%'><span style='white-space:nowrap'><strong>" + homeStart + "</strong>" + "<p><a href='javascript:void(0);' class='mz-copy-xml-btn' data-side='home'>" + getText('copyHome') + "</a></p></span></td>" + "<td align='center' valign='middle' width='40'><strong class='time'>0'</strong></td>" + "<td align='left' width='50%'><span style='white-space:nowrap'><strong>" + awayStart + "</strong>" + "<p><a href='javascript:void(0);' class='mz-copy-xml-btn' data-side='away'>" + getText('copyAway') + "</a></p></span></td></tr>"; timeline.find('tr:first').before(initRow); // CopyXML buton olayları $(document).off('click', '.mz-copy-xml-btn').on('click', '.mz-copy-xml-btn', function (e) { e.preventDefault(); const side = $(this).data('side'); copyXML(data, side === 'home', matchId); }); } } }); } function injectRButton() { if ($('#mz-match-result-btn').length > 0) return; // Match not started check const pageText = $('body').text(); if (pageText.includes('henüz başlamadı') || pageText.includes('not started') || pageText.includes('hasn\'t started')) return; // Match ID'yi URL'den bul const midMatch = window.location.href.match(/[?&]mid=([\d]+)/); if (!midMatch) return; const matchId = midMatch[1]; // Butonu oluştur - scoreboard alanındaki butonlarla uyumlu stil const $btn = $('<a></a>') .attr('href', 'javascript:void(0);') .attr('id', 'mz-match-result-btn') .attr('title', getText('rBtnTitle')) .css({ display: 'inline-block', marginRight: '4px', background: '#2d6a4f', color: '#fff', padding: '3px 10px', borderRadius: '4px', fontWeight: 'bold', fontSize: '14px', cursor: 'pointer', verticalAlign: 'middle', textDecoration: 'none', lineHeight: '1.4' }) .text('S'); $btn.on('click', function (e) { e.preventDefault(); showMatchResult(matchId); }); // Strateji 1: type=animation veya id*=animate olan bağlantıyı bul — parent'a prepend ile A'nın soluna ko const $animBtn = $('a[href*="type=animation"], a[id*="animate"], a#dg_animate_link').first(); if ($animBtn.length) { $animBtn.parent().prepend($btn); return; } // Strateji 2: 2D butonunu bul — parent container'a prepend yap (en başa / A'nın soluna) const $btn2d = $('a[href*="type=2d"]').first(); if ($btn2d.length) { $btn2d.parent().prepend($btn); return; } // Strateji 3: Metni tam "A" olan bağlantının önüne ekle let inserted = false; $('a, button').each(function () { const txt = $(this).text().trim(); if (txt === 'A') { $(this).before($btn); inserted = true; return false; } }); if (inserted) return; // Strateji 4: Scoreboard div'ine ekle const $scoreboard = $('div.scoreboard, div.scoreboard.shadow').first(); if ($scoreboard.length) { $scoreboard.prepend($btn); return; } // Strateji 5: İstatistik tablosunun üstüne ekle const $facts = $('#match-tactic-facts-wrapper').first(); if ($facts.length) { $facts.before($('<div style="margin-bottom:5px;padding:3px;"></div>').append($btn)); return; } // Strateji 6 (son çare): contentDiv'in başına ekle $('#contentDiv').first().prepend($('<div style="margin:5px 0;"></div>').append($btn)); } // Başlat: DOM'u izleyerek scoreboard veya maç içeriği yüklenince dene let _rBtnAttempts = 0; const waitForRBtnTarget = setInterval(function () { _rBtnAttempts++; const hasContent = $('div.scoreboard, #match-tactic-facts-wrapper, table.hitlist.statsLite, a[href*="type=2d"], a[href*="type=animation"]').length > 0; if (hasContent || _rBtnAttempts >= 20) { clearInterval(waitForRBtnTarget); if (hasContent || _rBtnAttempts >= 20) injectRButton(); } }, 500); // Maksimum 10 saniye bekle setTimeout(function () { clearInterval(waitForRBtnTarget); }, 10000); } /**************************************************************************************** * * * BÖLÜM 25: SCOUT BAZLI ANTRENMAN PLANI ÖNERİSİ (van.mz portlanmış) * * * ****************************************************************************************/ function initializeScoutTrainingPlan() { 'use strict'; const $ = jQuery; // Sadece oyuncu listesi, shortlist veya oyuncu profil sayfasında çalış const href = window.location.href; const isPlayersPage = href.indexOf('p=players') !== -1 && href.indexOf('pid=') === -1; const isShortlistPage = href.indexOf('p=shortlist') !== -1; const isPlayerProfilePage = href.indexOf('p=players') !== -1 && href.indexOf('pid=') !== -1; if (!isPlayersPage && !isShortlistPage && !isPlayerProfilePage) return; const lang = Core.i18n.lang; const skillKeys = { 1: 'skillSpeed', 2: 'skillStamina', 3: 'skillIntelligence', 4: 'skillPassing', 5: 'skillShooting', 6: 'skillHeading', 7: 'skillKeeping', 8: 'skillControl', 9: 'skillTackling', 10: 'skillCrossing', 11: 'skillSetPlays' }; const posKeys = { '9': 'posCB', '7': 'posGK', '10': 'posWinger', '5': 'posStriker', '4': 'posMF', '56': 'posPivot', '21': 'posDM', '22': 'posWB', '23': 'posAMF' }; const t = (key) => Core.i18n.getText(key); const attr = (id) => t(skillKeys[id]) || ('Attr' + id); const pos = (key) => t(posKeys[key]) || key; // van.mz'den: pozisyon → antrenman profilleri const scoutLocList = { "9": { Prop: { "1": 0, "2": 0, "9": 1, "10": 2, "8": 2, "3": 2, "4": 2, "6": 2 }, Order: [1, 2, 9, 10, 8, 3, 4, 6], CampKey: "9" }, "7": { Prop: { "2": 0, "7": 1, "3": 1, "1": 1, "10": 2, "11": 2, "8": 2 }, Order: [2, 7, 3, 1, 10, 11, 8], CampKey: "7" }, "10": { Prop: { "1": 0, "2": 0, "10": 1, "8": 1, "9": 2, "4": 2, "5": 2, "6": 2 }, Order: [1, 2, 10, 8, 9, 4, 5, 6], CampKey: "10" }, "5": { Prop: { "1": 0, "2": 0, "5": 1, "8": 1, "6": 1, "3": 2, "4": 2, "10": 2, "9": 2 }, Order: [1, 2, 5, 8, 6, 3, 4, 10, 9], CampKey: "5" }, "4": { Prop: { "1": 0, "2": 0, "4": 1, "8": 1, "3": 1, "10": 2, "9": 2, "5": 2, "6": 2 }, Order: [1, 2, 4, 8, 3, 10, 9, 5, 6], CampKey: "4" }, "5,6": { Prop: { "2": 0, "1": 0, "5": 1, "6": 1, "8": 1, "3": 2, "4": 2, "10": 2, "9": 2 }, Order: [2, 1, 5, 8, 6, 3, 4, 10, 9], CampKey: "56" } }; const OKeys = ["9", "7", "10", "5", "4", "5,6"]; function getProp(id, loc) { return loc.Prop[id] !== undefined ? loc.Prop[id] : 3; } function checkScoutLoc(lst, key, LP1, LP2, slocs) { if (lst[key] !== undefined) { const sloc = lst[key]; if (getProp(LP1, sloc) === 0 || getProp(LP2, sloc) === 0) { slocs.push({ type: 1, loc: sloc }); } else { slocs.push({ type: 0, loc: sloc }); } slocs.keys[key] = true; } } function getTrainPlans(HP1, HP2, LP1, LP2) { const slocs = []; slocs.keys = {}; checkScoutLoc(scoutLocList, HP1 + ',' + HP2, LP1, LP2, slocs); checkScoutLoc(scoutLocList, HP2 + ',' + HP1, LP1, LP2, slocs); checkScoutLoc(scoutLocList, String(HP1), LP1, LP2, slocs); checkScoutLoc(scoutLocList, String(HP2), LP1, LP2, slocs); for (let i = 0; i < OKeys.length; i++) { const key = OKeys[i]; if (!slocs.keys[key]) { const loc = scoutLocList[key]; if (getProp(LP1, loc) !== 1 && getProp(LP2, loc) !== 1) { slocs.push({ type: 2, loc: loc }); slocs.keys[key] = true; } } } if (slocs.length === 0) { slocs.push({ type: 3, loc: scoutLocList[OKeys[0]] }); } return slocs; } function isLoser(HStar, LStar, LP1, LP2) { if (HStar <= 2) return true; if (LStar === 1 && HStar < 4) { if (LP1 === 1 || LP2 === 1 || LP1 === 2 || LP2 === 2) return true; } return false; } function getTrainPRI(sloc, HStar, HP1, HP2, LStar, LP1, LP2) { const lstBase = [], lstMain = [], lstSub = []; for (let i = 0; i < sloc.Order.length; i++) { const ID = sloc.Order[i]; const t = getProp(ID, sloc); if (t === 0 || t === 1) { if (HStar >= 3) { if (HP1 === ID || HP2 === ID) { lstSub.push(ID); continue; } } if (LStar <= 2) { if (LP1 === ID || LP2 === ID) { if (t === 0) lstBase.unshift(ID); else lstMain.push(ID); continue; } } if (HP1 !== ID && HP2 !== ID && LP1 !== ID && LP2 !== ID) { if (ID === 3) lstSub.push(ID); else if (t === 0) lstBase.push(ID); else lstMain.push(ID); } } else if (t === 2) { lstSub.push(ID); } } let pri = 29; const ditPRI = {}; const lstOrder = lstBase.concat(lstMain).concat(lstSub); lstBase.forEach(id => { ditPRI[id] = pri--; }); pri = 19; lstMain.forEach(id => { ditPRI[id] = pri--; }); pri = 9; lstSub.forEach(id => { ditPRI[id] = pri--; }); return { Sloc: sloc, TrainPRI: ditPRI, Order: lstOrder }; } function showSuggestionModal(pid, HS, HPIds, LS, LPIds, remark) { $('#mz-scout-plan-modal').remove(); let strHtml = ''; if (remark) strHtml += '<p style="margin-bottom:8px; font-style:italic;">' + remark + '</p>'; if (isLoser(HS, LS, LPIds[0], LPIds[1])) { strHtml += '<p style="color:#dc3545; font-weight:bold;">' + t('loserMsg') + '</p>'; } const plans = getTrainPlans(HPIds[0], HPIds[1], LPIds[0], LPIds[1]); plans.forEach(function (plan, planIndex) { const pri = getTrainPRI(plan.loc, HS, HPIds[0], HPIds[1], LS, LPIds[0], LPIds[1]); const orderStr = pri.Order.map(id => attr(id)).join(' → '); const pTypeStr = (planIndex + 1) + '. ' + t('option') + ':'; const pPosStr = pos(pri.Sloc.CampKey); strHtml += '<p><strong>' + pTypeStr + ' ' + pPosStr + '</strong></p>' + '<p style="color:#00529B;"><strong style="color:#2d6a4f;">' + t('trainOrder') + '</strong> ' + orderStr + '</p>'; }); const modal = $('<div id="mz-scout-plan-modal"></div>').css({ position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', background: '#fff', padding: '20px', borderRadius: '8px', boxShadow: '0 5px 20px rgba(0,0,0,0.4)', zIndex: 99999, maxWidth: '500px', width: '90%', maxHeight: '80vh', overflowY: 'auto', fontFamily: 'sans-serif', fontSize: '14px' }); modal.html('<h3 style="margin-top:0; color:#00529B;">' + t('sugTitle') + '</h3>' + strHtml + '<div style="text-align:right; margin-top:15px;">' + '<button id="mz-scout-plan-close" style="background:#00529B; color:#fff; border:none; padding:6px 16px; border-radius:4px; cursor:pointer;">' + t('close') + '</button></div>'); $('body').append(modal); $('#mz-scout-plan-close').on('click', function () { modal.remove(); }); } // Belirli bir pid için izci raporunu çek ve etiketi ekle const _scoutCache = {}; function fetchScoutAndInject(pid, $container) { if (_scoutCache[pid]) return; // Zaten işlendi _scoutCache[pid] = true; const url = '/ajax.php?p=players&sub=scout_report&pid=' + pid + '&sport=soccer'; GM_xmlhttpRequest({ method: 'GET', url: 'https://www.managerzone.com' + url, onload: function (resp) { if (!resp.responseText) return; const srdom = $($.parseHTML(resp.responseText)); const remark = srdom.find('span.blurred span').text(); const srdivs = srdom.find('dd div.flex-grow-1'); if (srdivs.length < 3) return; const HS = srdivs.eq(0).find('.lit').length; const LS = srdivs.eq(1).find('.lit').length; const SS = srdivs.eq(2).find('.lit').length; const getNames = ($div) => { const arr = []; $div.find('.blurred span').each(function () { const txt = $(this).text().trim(); if (txt && txt !== 'Trzxyvopaxis') arr.push(txt); }); return arr; }; const HArr = getNames(srdivs.eq(0)); const LArr = getNames(srdivs.eq(1)); // Skill isimlerini ve numaralarını eşleştir let skillnames = $container.find('div.player-skills').find('span.responsive-hide.responsive-container'); if (skillnames.length === 0) skillnames = $container.find('td > span.clippable'); if (skillnames.length === 0) skillnames = $container.find('td > span.skill_name'); const HPIds = [], LPIds = []; skillnames.each(function (i) { const txt = $(this).text().trim(); if (HArr.indexOf(txt) >= 0) { HPIds.push(i + 1); } else if (LArr.indexOf(txt) >= 0) { LPIds.push(i + 1); } }); if (HPIds.length === 0 && LPIds.length === 0) return; // Skill eşleşmedi // Header'daki eski scout tag'i kaldır (artık kullanılmıyor) $('#mz-scout-tag-' + pid).remove(); // 'Görünümü Değiştir' + 'İsim Değişikliği' (U18) dahil tüm görünüm/isim butonlarını bul const btnId = 'mz-ant-btn-' + pid; if ($('#' + btnId).length === 0) { // Sadece metin eşleşmesiyle bul — genel href/class seçiciler yanlış eleman yakalayabilir const $allChangeBtns = $container.find('a, button').filter(function () { const txt = $(this).text().trim(); return txt === 'Görünümü Değiştir' || txt === 'Change look' || txt === 'Change appearance' || txt === 'İsim Değişikliği' || txt === 'Change name'; }); const $antBtn = $('<button id="' + btnId + '" class="mz-ant-onerisi-btn">' + '[Antrenman Önerisi]</button>').css({ display: 'block', marginTop: '6px', padding: '4px 10px', fontSize: '12px', fontWeight: 'bold', background: '#00529B', color: '#fff', border: '1px solid #003d75', borderRadius: '4px', cursor: 'pointer', width: 'auto' }).on('click', function (e) { e.preventDefault(); e.stopPropagation(); showSuggestionModal(pid, HS, HPIds, LS, LPIds, remark); }); if ($allChangeBtns.length > 0) { // En son eşleşen butona (örn. 'İsim Değişikliği') direkt .after() ile ekle const $wrapper = $('<div></div>').css({ marginTop: '6px' }).append($antBtn); $allChangeBtns.last().after($wrapper); } else { // Son çare: container'daki buton/link alanına ekle $container.find('div.player-buttons, div[class*="button"]').last().append($antBtn); } } }, onerror: function (e) { console.error('[MZone Scout] İzci raporu çekme hatası (pid:' + pid + '):', e); } }); } // PERFORMANS DÜZELTMESİ: processAllPlayers tamamlandıktan sonra // observer'ı kapat ve tekrar DOM taraması yapma. let _processAllDone = false; let _scoutObserverRef = null; function processAllPlayers() { if (_processAllDone) return; // Tüm oyuncular işlendi, tekrar tarama yapma let newPlayerFound = false; $('.playerContainer').each(function () { const $c = $(this); const pid = $c.find('span.player_id_span').text().trim(); if (pid && !_scoutCache[pid]) { newPlayerFound = true; fetchScoutAndInject(pid, $c); } }); // Hiç yeni oyuncu kalmadıysa observer'ı kapat if (!newPlayerFound) { _processAllDone = true; if (_scoutObserverRef) { _scoutObserverRef.disconnect(); _scoutObserverRef = null; } } } // Tekli profil sayfasında çalış (?p=players&pid=...) function processSinglePlayerPage() { const pidMatch = window.location.href.match(/[?&]pid=(\d+)/); if (!pidMatch) return; const pid = pidMatch[1]; if ($('#mz-ant-btn-' + pid).length > 0) return; // Profil sayfasında geniş container — buton bulma zaten metin bazlı const $container = $('#contentDiv').length ? $('#contentDiv') : $('body'); fetchScoutAndInject(pid, $container); } // Sayfa türüne göre başlat if (isPlayerProfilePage) { // Profil sayfası: DOM tamamen yüklenince çalış setTimeout(processSinglePlayerPage, 2000); } else { // Liste / shortlist sayfası: tüm playerContainer'ları işle setTimeout(processAllPlayers, 1500); // Eski tarz setInterval kullanımı (Scroll JANK'i kalıcı olarak engeller) setInterval(processAllPlayers, 1500); } } /**************************************************************************************** * * * BÖLÜM 26: TAKTİK SAYFASI — XML OTODOlDURMA + KOORDİNAT PANELİ + COPY/PASTE * * * ****************************************************************************************/ // ======================================================================================== // ORTAK KULLANIM: Taktikler Geçmişi Modalı // ======================================================================================== window._mzShowTacticHistoryModal = function (isFromMatchPage = false) { const $ = jQuery; let history = []; try { history = JSON.parse(localStorage.getItem('mz_tactic_history') || '[]'); } catch (e) { } if (history.length === 0) { alert("Henüz XML Kopyala özelliği ile hafızaya alınmış bir taktik bulunmuyor."); return; } $('#mz-copy-success-modal').remove(); const trMap = { 'normal': 'Normal', 'defensive': 'Defansif', 'offensive': 'Ofansif', 'passing': 'Kısa pas', 'shortpass': 'Kısa pas', 'longballs': 'Uzun top', 'longball': 'Uzun top', 'wings': 'Kanatları kullan', 'passive': 'Pasif', 'aggressive': 'Agresif' }; let historyHtml = ''; history.forEach((item, index) => { const t_tactic = trMap[item.tactic] || item.tactic || '-'; const t_playstyle = trMap[item.playstyle] || item.playstyle || '-'; const t_aggression = trMap[item.aggression] || item.aggression || '-'; let scoreHtml = item.matchScore || '-'; if (item.teamName && scoreHtml.includes(item.teamName)) { // Kopyalanan takımın ismini skor içinde vurgula const hlColor = item.isHome ? '#d97706' : '#2563eb'; // Ev: Turuncu, Dep: Mavi const bgColor = item.isHome ? '#fef3c7' : '#dbeafe'; scoreHtml = scoreHtml.replace( item.teamName, `<span style="color: ${hlColor}; font-weight: 900; background: ${bgColor}; padding: 1px 4px; border-radius: 3px;">${item.teamName}</span>` ); } // Maç sayfasından kopyalandıysa en üsttekine badge ekle const isJustCopied = isFromMatchPage && index === 0; const copiedBadgeHtml = isJustCopied ? `<div style="position: absolute; top: -12px; right: 10px; background: #28a745; color: white; padding: 4px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; box-shadow: 0 2px 5px rgba(0,0,0,0.2); z-index: 10; border: 2px solid white;">ŞU AN KOPYALANAN</div>` : ''; const containerBorder = isJustCopied ? 'border: 2px solid #28a745;' : 'border: 1px solid #e9ecef;'; const containerBg = isJustCopied ? 'background: #f0fff4;' : 'background: #f8f9fa;'; historyHtml += ` <div style="${containerBg} ${containerBorder} padding: 12px; border-radius: 8px; margin-bottom: 10px; text-align: left; position: relative; display: flex; justify-content: space-between;"> ${copiedBadgeHtml} <div style="flex: 1;"> <p style="margin: 0 0 5px 0; color: #2d6a4f; font-size: 15px; font-weight: bold;">${item.teamName}</p> <p style="margin: 3px 0; color: #333; font-size: 13px;"><strong>Skor:</strong> ${scoreHtml}</p> <p style="margin: 3px 0; color: #333; font-size: 13px;"><strong>Ayarlar:</strong> ${t_tactic} | ${t_playstyle} | ${t_aggression}</p> <p style="margin: 5px 0 0 0; color: #888; font-size: 11px;">${item.date}</p> <div style="margin-top: 8px; display: flex; gap: 8px;"> <button class="mz-history-copy-btn" data-xml="${encodeURIComponent(item.xml)}" style="background: #6c757d; color: #fff; border: none; padding: 5px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; transition: background 0.2s;">XML'i Kopyala</button> <button class="mz-history-delete-btn" data-id="${item.id}" style="background: #e63946; color: #fff; border: none; padding: 5px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; transition: background 0.2s;">Sil</button> </div> </div> ${item.matchId ? ` <div style="margin-left: 15px; display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 120px;"> <img src="/dynimg/pitch.php?match_id=${item.matchId}" class="shadow" alt="Saha" style="width: 120px; border-radius: 4px; border: 1px solid #ccc; background:#295c25;"> <span style="font-size: 10px; color: #333; margin-top: 6px; font-weight: bold; background: ${item.isHome ? '#fff3cd' : '#e2e3e5'}; border: 1px solid ${item.isHome ? '#ffeeba' : '#dae0e5'}; padding: 3px 6px; border-radius: 4px; text-align: center;"> ${item.isHome ? '🟡 Sarı Noktalar<br><span style="font-size:9px;color:#d97706;">(Ev Sahibi)</span>' : '⚫ Siyah Noktalar<br><span style="font-size:9px;color:#2563eb;">(Deplasman)</span>'} </span> </div>` : ''} </div> `; }); // Taktikler Sayfasına Git Butonu (Sadece maç sayfasından açıldığında) let footerBtnHtml = ''; if (isFromMatchPage) { footerBtnHtml = ` <div style="margin-top: 15px;"> <button onclick="window.location.href='/?p=tactics';" style="width: 100%; padding: 12px; background: #1b4332; color: #fff; font-size: 16px; font-weight: bold; border: none; border-radius: 6px; cursor: pointer; transition: background 0.2s; box-shadow: 0 4px 6px rgba(0,0,0,0.1);"> Taktikler Sayfasına Git </button> </div> `; } const modalHtml = ` <div id="mz-copy-success-modal" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); display: flex; justify-content: center; align-items: center; z-index: 999999;"> <div style="background: #fff; padding: 20px 25px; border-radius: 12px; box-shadow: 0 5px 20px rgba(0,0,0,0.2); text-align: center; max-width: 500px; width: 90%; max-height: 85vh; display: flex; flex-direction: column; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; position: relative;"> <span id="mz-copy-modal-close" style="position: absolute; top: 10px; right: 15px; font-size: 24px; color: #888; cursor: pointer; font-weight: bold;">×</span> <div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #e9ecef; padding-bottom: 10px; margin: 10px 0 15px 0;"> <h2 style="color: #2d6a4f; margin: 0; font-size: 20px;">Taktikler Geçmişi</h2> <button id="mz-clear-history-btn-tactics" style="background: #dc3545; color: #fff; border: none; padding: 5px 10px; border-radius: 4px; font-size: 12px; cursor: pointer; transition: background 0.2s;">Tümünü Sil</button> </div> <div style="overflow-y: auto; flex: 1; padding-right: 5px; margin-bottom: 5px; min-height: 200px;"> ${historyHtml} </div> ${footerBtnHtml} </div> </div> `; $('body').append(modalHtml); $('#mz-copy-modal-close').on('click', function () { $('#mz-copy-success-modal').remove(); }); $('#mz-copy-success-modal').on('click', function (e) { if (e.target.id === 'mz-copy-success-modal') { $(this).remove(); } }); // Geçmişi temizleme $('#mz-clear-history-btn-tactics').on('click', function () { if (confirm('Tüm taktik geçmişini silmek istediğinize emin misiniz?')) { localStorage.removeItem('mz_tactic_history'); $('#mz-copy-success-modal').remove(); alert('Geçmiş temizlendi.'); } }); // Tekil silme $('.mz-history-delete-btn').on('click', function () { const idToDelete = $(this).data('id'); const $itemRow = $(this).closest('div[style*="border-radius: 8px"]'); let currentHistory = []; try { currentHistory = JSON.parse(localStorage.getItem('mz_tactic_history') || '[]'); } catch (e) { } const newHistory = currentHistory.filter(h => h.id !== idToDelete); localStorage.setItem('mz_tactic_history', JSON.stringify(newHistory)); $itemRow.fadeOut(300, function () { $(this).remove(); if (newHistory.length === 0) $('#mz-copy-success-modal').remove(); }); }); $('.mz-history-copy-btn').hover( function () { $(this).css('background', '#5a6268'); }, function () { $(this).css('background', '#6c757d'); } ).on('click', function () { const xmlToCopy = decodeURIComponent($(this).data('xml')); const btn = $(this); // Geçmişten kopyalanan taktiğin ayarlarını da autofill için sakla try { const matchedItem = history.find(h => h.xml === xmlToCopy); if (matchedItem) { const tacticSettings = { tactics: matchedItem.tactic, playstyle: matchedItem.playstyle, aggression: matchedItem.aggression }; localStorage.setItem('mz_autofill_tactic', JSON.stringify(tacticSettings)); } } catch (e) { } if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(xmlToCopy).then(() => { btn.text('Kopyalandı! ✓').css('background', '#28a745'); setTimeout(() => btn.text("XML'i Kopyala").css('background', '#6c757d'), 2000); }); } else { const ta = document.createElement('textarea'); ta.value = xmlToCopy; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); btn.text('Kopyalandı! ✓').css('background', '#28a745'); setTimeout(() => btn.text("XML'i Kopyala").css('background', '#6c757d'), 2000); } }); }; function initializeTacticsAutoFill() { 'use strict'; const $ = jQuery; // Sadece taktik editör sayfasında çalış if (window.location.href.indexOf('p=tactics') === -1) return; const lang = Core.i18n.lang; // --- Ortak: Taktik Tercüme Mapleri --- const tacticDisplay = { wings: t('tacticWings'), shortpass: t('tacticShortPass'), longball: t('tacticLongBall') }; const playstyleDisplay = { offensive: t('styleOffensive'), normal: 'Normal', defensive: t('styleDefensive') }; const aggrDisplay = { aggressive: t('aggrAggressive'), normal: 'Normal', passive: t('aggrPassive') }; // --- Yardımcı: select elementlerini doldur --- function trySetDropdown(tactics, playstyle, aggression) { let filledCount = 0; // Aktif sekmenin harfini bul (örneğin "tacticTab_l" -> "l") let activeLetter = ''; const activeTabId = $('.ui-tabs-selected').attr('id'); if (activeTabId && activeTabId.startsWith('tacticTab_')) { activeLetter = activeTabId.replace('tacticTab_', ''); } // Temel selector'lar let selTactics = ['select[data-property="tactics"]', 'select.tactics-select', '#tactics-select']; let selStyle = ['select[data-property="playstyle"]', 'select.playstyle-select', '#playstyle-select']; let selAggr = ['select[data-property="aggression"]', 'select.aggression-select', '#aggression-select']; // Eğer aktif sekme varsa, sadece o sekmeye ait dropdown'ları ekle if (activeLetter) { selTactics.push('#tactics_' + activeLetter); selStyle.push('#style_' + activeLetter); selAggr.push('#aggression_' + activeLetter); } else { // Bulunamazsa varsayılan fallback selTactics.push('#tactics_l'); selStyle.push('#style_l'); selAggr.push('#aggression_l'); } // Sadece ekranda GÖRÜNÜR olan dropdownları değiştir $(selTactics.join(', ')).filter(':visible').each(function () { $(this).val(tactics); this.dispatchEvent(new Event('change', { bubbles: true })); filledCount++; }); $(selStyle.join(', ')).filter(':visible').each(function () { $(this).val(playstyle); this.dispatchEvent(new Event('change', { bubbles: true })); filledCount++; }); $(selAggr.join(', ')).filter(':visible').each(function () { $(this).val(aggression); this.dispatchEvent(new Event('change', { bubbles: true })); filledCount++; }); // Eğer üstteki spesifik ID'ler hiçbir şey bulamadıysa, ekrandaki görünür select'leri tara if (filledCount === 0) { $('select:visible').each(function () { const $sel = $(this); const $opts = $sel.find('option'); const optVals = $opts.map(function () { return $(this).val(); }).get(); if (tactics && optVals.indexOf(tactics) !== -1 && (optVals.indexOf('wings') !== -1 || optVals.indexOf('shortpass') !== -1 || optVals.indexOf('longball') !== -1)) { if ($sel.val() !== tactics) { $sel.val(tactics); this.dispatchEvent(new Event('change', { bubbles: true })); filledCount++; } } if (playstyle && optVals.indexOf(playstyle) !== -1 && (optVals.indexOf('offensive') !== -1 || optVals.indexOf('defensive') !== -1)) { if ($sel.val() !== playstyle) { $sel.val(playstyle); this.dispatchEvent(new Event('change', { bubbles: true })); filledCount++; } } if (aggression && optVals.indexOf(aggression) !== -1 && (optVals.indexOf('aggressive') !== -1 || optVals.indexOf('passive') !== -1)) { if ($sel.val() !== aggression) { $sel.val(aggression); this.dispatchEvent(new Event('change', { bubbles: true })); filledCount++; } } }); } return filledCount; } // --- Yardımcı: Toast bildirimi --- function showToast(msg, color) { color = color || '#2ecc71'; $('.mz-autofill-toast').remove(); const $toast = $('<div class="mz-autofill-toast">').css({ position: 'fixed', top: '60px', left: '50%', transform: 'translateX(-50%)', background: 'rgba(0,0,0,0.88)', color: color, padding: '10px 22px', borderRadius: '8px', fontSize: '14px', fontWeight: 'bold', zIndex: 100000, boxShadow: '0 4px 12px rgba(0,0,0,0.5)', border: '1px solid ' + color, fontFamily: 'Segoe UI, sans-serif', textAlign: 'center' }).html(msg).appendTo('body'); setTimeout(function () { $toast.fadeOut(400, function () { $toast.remove(); }); }, 4500); } // --- Yardımcı: Taktik ayarlarını uygula ve kullanıcıya bildir --- function applyAndNotify(tacticData) { if (!tacticData) return; let uiTactics = ''; // Paslaşma: shortpass, longball, wings let uiPlaystyle = ''; // Oyun Stili: offensive, defensive, normal let uiAggression = tacticData.aggression || ''; // Agresiflik: passive, normal, aggressive // XML'in nereden kopyalandığına göre 'tactics' ve 'playstyle' özellikleri yer değiştirebiliyor. // Bu yüzden değerlerine bakarak hangisinin paslaşma, hangisinin oyun stili olduğunu tespit ediyoruz. const vals = [tacticData.tactics || '', tacticData.playstyle || '']; vals.forEach(val => { if (!val) return; const lower = val.toLowerCase(); // Paslaşma (Passing) tespiti if (lower === 'passing' || lower === 'shortpass') uiTactics = 'shortpass'; else if (lower === 'longballs' || lower === 'longball') uiTactics = 'longball'; else if (lower === 'wings') uiTactics = 'wings'; // Oyun Stili (Mentality) tespiti else if (lower === 'offensive' || lower === 'attacking') uiPlaystyle = 'offensive'; else if (lower === 'defensive') uiPlaystyle = 'defensive'; else if (lower === 'normal' || lower === 'balanced') uiPlaystyle = 'normal'; }); if (!uiTactics && !uiPlaystyle && !uiAggression) return; const count = trySetDropdown(uiTactics, uiPlaystyle, uiAggression); const infoLine = '<span style="color:#fff">' + t('statMappings')['N'] + ': ' + '<b>' + (tacticDisplay[uiTactics] || uiTactics) + '</b> | ' + t('statistics') + ': ' + '<b>' + (playstyleDisplay[uiPlaystyle] || uiPlaystyle) + '</b> | ' + t('aggrAggressive') + ': ' + '<b>' + (aggrDisplay[uiAggression] || uiAggression) + '</b>' + '</span>'; if (count > 0) { showToast(t('tacticAutoFilled') + '<br>' + infoLine, '#2ecc71'); } else { showToast(t('tacticEnterManually') + '<br>' + infoLine, '#f39c12'); } try { localStorage.removeItem('mz_autofill_tactic'); } catch (e2) { } } // --- 1) Sayfa açılışında: mevcut localStorage verisini kontrol et --- setTimeout(function () { try { const raw = localStorage.getItem('mz_autofill_tactic'); if (raw) applyAndNotify(JSON.parse(raw)); } catch (e) { } }, 1200); // --- 2) Gerçek zamanlı: Başka sekmede "XML Kopyala" tıklanınca anında algıla --- window.addEventListener('storage', function (e) { if (e.key !== 'mz_autofill_tactic' || !e.newValue) return; try { const incoming = JSON.parse(e.newValue); setTimeout(function () { applyAndNotify(incoming); }, 300); } catch (e2) { } }); // ===================================================================== // KOORDİNAT PANELİ + FORMASYON COPY/PASTE // MZ Axis mantığından uyarlanmıştır (jrcl), MZone'a entegre edildi // ===================================================================== (function initCoordPanel() { // MZ Axis paneli zaten varsa kendi panelimizi ekle (çakışmaz, bağımsız çalışır) if (document.getElementById('mz-coord-panel')) return; // Koordinat dönüşüm yardımcıları function coordsToPitch(x, y) { return { left: x + 97, top: 155 - y }; } function pitchToCoords(l, t) { return { x: Math.round(l) - 97, y: 155 - Math.round(t) }; } function getActivePlayer() { return document.querySelector('.fieldpos.ui-selected'); } function getFieldPlayers() { return document.querySelectorAll( '.fieldpos.fieldpos-ok.ui-draggable:not(.substitute):not(.goalkeeper)' ); } let _inputX, _inputY; // ----- Panel HTML'i oluştur ----- function createPanel() { const container = document.getElementById('formation-container'); if (!container || document.getElementById('mz-coord-panel')) return; const panel = document.createElement('div'); panel.id = 'mz-coord-panel'; const isMobile = window.innerWidth <= 900; if (isMobile) { panel.style.cssText = 'display: grid !important; grid-template-columns: 1fr 1fr !important; gap: 6px !important; width: 100% !important; margin-top: 10px !important; padding: 6px !important; background: #ffffff !important; border: 1px solid #2e7d32 !important; border-radius: 4px !important; box-sizing: border-box !important;'; panel.innerHTML = `<div style="grid-column: span 2; display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 4px; padding-bottom: 4px; border-bottom: 1px solid #eee;">` + `<span style="color:#333; font-weight: bold; font-size: 12px;">📍 ${t('location')}:</span>` + `<div style="display:flex; align-items:center; gap:2px;"><span style="color:green; font-weight:bold; font-size:11px;">X</span> <input id="mz-input-x" type="text" value="--" disabled style="width:35px; text-align:center; padding:2px; border:1px solid #ccc; border-radius:4px; background:#f9f9f9; font-size:11px;"></div>` + `<div style="display:flex; align-items:center; gap:2px;"><span style="color:blue; font-weight:bold; font-size:11px;">Y</span> <input id="mz-input-y" type="text" value="--" disabled style="width:35px; text-align:center; padding:2px; border:1px solid #ccc; border-radius:4px; background:#f9f9f9; font-size:11px;"></div>` + `</div>` + `<button id="mz-copy-btn" style="padding:4px; font-weight:bold; background:#555; color:#fff; border:none; border-radius:4px; cursor:pointer; font-size:11px; height:26px;">${t('copy')}</button>` + `<button id="mz-paste-btn" style="padding:4px; font-weight:bold; background:#00529B; color:#fff; border:none; border-radius:4px; cursor:pointer; font-size:11px; height:26px;">${t('paste')}</button>` + `<button id="mz-rival-btn" style="padding:4px; font-weight:bold; background:#7b2d8b; color:#fff; border:none; border-radius:4px; cursor:pointer; font-size:11px; height:26px;">${t('rival')}</button>` + `<button id="mz-history-btn" style="padding:4px; font-weight:bold; background:#1b4332; color:#fff; border:none; border-radius:4px; cursor:pointer; font-size:11px; height:26px;">Geçmiş</button>`; } else { panel.style.cssText = 'display:inline-flex; align-items:center; gap:4px; margin-left:8px; font-size:12px; font-weight:600;'; panel.innerHTML = `<span style="color:#333">${t('location')}:</span>` + '<span style="color:green">X</span>' + '<input id="mz-input-x" type="text" value="--" disabled style="width:28px; text-align:center; font-size:11px; border:1px solid #aaa; border-radius:3px; padding:1px 3px;">' + '<span style="color:blue">Y</span>' + '<input id="mz-input-y" type="text" value="--" disabled style="width:28px; text-align:center; font-size:11px; border:1px solid #aaa; border-radius:3px; padding:1px 3px;">' + `<button id="mz-copy-btn" style="padding:2px 7px; font-size:11px; background:#555; color:#fff; border:none; border-radius:3px; cursor:pointer;" title="${t('copy')}">${t('copy')}</button>` + `<button id="mz-paste-btn" style="padding:2px 7px; font-size:11px; background:#00529B; color:#fff; border:none; border-radius:3px; cursor:pointer;" title="${t('paste')}">${t('paste')}</button>` + `<button id="mz-rival-btn" style="padding:2px 7px; font-size:11px; background:#7b2d8b; color:#fff; border:none; border-radius:3px; cursor:pointer; margin-left:6px;" title="${t('rival')}">🕵️ ${t('rival')}</button>` + `<button id="mz-history-btn" style="padding:2px 7px; font-size:11px; background:#1b4332; color:#fff; border:none; border-radius:3px; cursor:pointer; margin-left:6px;" title="Önceden Kopyalanan Taktikler">📋 Geçmiş</button>`; } container.appendChild(panel); _inputX = document.getElementById('mz-input-x'); _inputY = document.getElementById('mz-input-y'); // Input'a yazınca seçili oyuncuyu taşı [_inputX, _inputY].forEach(function (inp) { inp.addEventListener('focus', function () { this.select(); }); inp.addEventListener('input', applyInputToPlayer); }); document.getElementById('mz-copy-btn').addEventListener('click', copyFormation); document.getElementById('mz-paste-btn').addEventListener('click', pasteFormation); document.getElementById('mz-rival-btn').addEventListener('click', function () { if (window._mzRivalIsOpen) { window._mzRivalClose && window._mzRivalClose(); } else { window._mzRivalShowModal && window._mzRivalShowModal(); } }); // Taktik geçmişi modalını açma butonu document.getElementById('mz-history-btn').addEventListener('click', function () { if (typeof window._mzShowTacticHistoryModal === 'function') { window._mzShowTacticHistoryModal(false); } }); } // ----- Panel güncelle ----- function updatePanel(x, y) { if (!_inputX || !_inputY) return; if (x === undefined) { _inputX.value = '--'; _inputX.disabled = true; _inputY.value = '--'; _inputY.disabled = true; } else { _inputX.value = x; _inputX.disabled = false; _inputY.value = y; _inputY.disabled = false; } } // ----- Input → oyuncu konumu ----- function applyInputToPlayer() { const player = getActivePlayer(); if (!player) return; const x = Number(_inputX.value); const y = Number(_inputY.value); if (isNaN(x) || isNaN(y)) return; if (x < -96 || x > 96 || y < -157 || y > 101) { this.style.backgroundColor = '#ffd6d6'; return; } this.style.backgroundColor = ''; const pos = coordsToPitch(x, y); player.style.left = pos.left + 'px'; player.style.top = pos.top + 'px'; } // ----- Formasyon kopyala ----- function copyFormation() { const players = Array.from(getFieldPlayers()); if (players.length !== 10) { showToast(t('formationError10Players'), '#f39c12'); return; } const coords = players .map(function (p) { const pos = pitchToCoords(p.offsetLeft, p.offsetTop); return { x: pos.x, y: pos.y }; }) .sort(function (a, b) { return a.y !== b.y ? a.y - b.y : a.x - b.x; }); const text = coords.map(function (c) { return c.x + ',' + c.y; }).join('\n'); navigator.clipboard.writeText(text) .then(function () { showToast(t('formationCopySuccess'), '#2ecc71'); }) .catch(function () { showToast(t('formationErrorClipboardFail'), '#e74c3c'); }); } // ----- Formasyon yapıştır ----- function pasteFormation() { navigator.clipboard.readText().then(function (text) { if (!text) { showToast(t('formationErrorClipboardEmpty'), '#f39c12'); return; } // formation dizisi her zaman PIXEL değerleri saklar [left, top] let formation = []; if (text.indexOf('xml') !== -1 || text.indexOf('XML') !== -1) { // XML formatı: attribute x,y → doğrudan style.left/top pixel değerleri (-7/-9 offset) const parser = new DOMParser(); const xmlDoc = parser.parseFromString(text, 'text/xml'); // Eğer XML içinde Team node'u varsa taktik ayarlarını da uygula const teamNodes = xmlDoc.getElementsByTagName('Team'); if (teamNodes.length > 0) { const tacticSettings = { tactics: teamNodes[0].getAttribute('tactics') || teamNodes[0].getAttribute('tactic') || '', playstyle: teamNodes[0].getAttribute('playstyle') || '', aggression: teamNodes[0].getAttribute('aggression') || '' }; if (typeof applyAndNotify === 'function') { applyAndNotify(tacticSettings); } } const posList = xmlDoc.getElementsByTagName('Pos'); for (let i = 1; i < posList.length; i++) { formation.push([ Number(posList[i].getAttribute('x')) - 7, // = style.left Number(posList[i].getAttribute('y')) - 9 // = style.top ]); } } else { // Düz x,y koordinat formatı (bizim koordinat sistemimiz) // coordsToPitch ile pixel'e çevirerek sakla const lines = text.trim().split('\n').filter(Boolean); if (lines.length !== 10) { showToast(t('formationErrorInvalidFormat'), '#f39c12'); return; } lines.forEach(function (line) { const parts = line.split(','); if (parts.length >= 2) { const pos = coordsToPitch(Number(parts[0]), Number(parts[1])); formation.push([pos.left, pos.top]); // piksel olarak sakla } }); } if (formation.length !== 10) { showToast(t('formationErrorNoCoords'), '#f39c12'); return; } // Y (top)'a göre sırala — büyük top değeri = sahanın altı = önce formation.sort(function (a, b) { return b[1] !== a[1] ? b[1] - a[1] : a[0] - b[0]; }); const fieldPlayers = Array.from(getFieldPlayers()); if (fieldPlayers.length !== 10) { showToast(t('formationError10Players'), '#f39c12'); return; } // Mevcut oyuncu sıralamasını style.left/top ile oku (offsetLeft değil) const currentOrder = fieldPlayers .map(function (p, i) { return { idx: i, left: Number((p.style.left || '0').replace('px', '')), top: Number((p.style.top || '0').replace('px', '')) }; }) .sort(function (a, b) { return b.top !== a.top ? b.top - a.top : a.left - b.left; }); // Pixel değerlerini direkt ata — coordsToPitch çağrısı YOK for (let i = 0; i < 10; i++) { const target = fieldPlayers[currentOrder[i].idx]; target.style.left = formation[i][0] + 'px'; target.style.top = formation[i][1] + 'px'; } showToast(t('formationPasteSuccess'), '#2ecc71'); }).catch(function () { showToast(t('formationErrorPasteFail'), '#e74c3c'); }); } // ----- MutationObserver: sürükleme & seçim takibi ----- function observePitch() { const pitch = document.getElementById('pitch'); if (!pitch) return; const obs = new MutationObserver(function (mutations) { mutations.forEach(function (m) { if (!m.target || !m.target.classList) return; if (!m.target.classList.contains('fieldpos')) return; if (m.type === 'attributes' && m.attributeName === 'style') { // Sürüklenince — sadece aktif oyuncu için paneli güncelle const active = getActivePlayer(); if (active === m.target) { const pos = pitchToCoords(m.target.offsetLeft, m.target.offsetTop); updatePanel(pos.x, pos.y); } } if (m.type === 'attributes' && m.attributeName === 'class') { // Seçim değişti const active = getActivePlayer(); if (!active) { updatePanel(); } else { const pos = pitchToCoords(active.offsetLeft, active.offsetTop); updatePanel(pos.x, pos.y); } } }); }); obs.observe(pitch, { subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); } // ----- Başlat: pitch ve formation-container yüklenince ----- function tryInit() { if (document.getElementById('formation-container') && document.getElementById('pitch')) { createPanel(); observePitch(); } else { setTimeout(tryInit, 500); } } tryInit(); })(); // ===================================================================== // RAKİP FORMASYON GÖRÜNTÜLEME // MZ Axis'ten FARKLI özgün tasarım: // • Styled modal (prompt() değil) // • Pozisyona göre renkli markerlar (for=kırmızı, mid=yeşil, def=mavi) // • Sağda bilgi kartı paneli // • Türkçe UI // ===================================================================== (function initRivalModule() { // Para birimi tespiti ve sembol eşleştirme function getUserCurrencyInfo() { var moneyEl = document.querySelector('#sub-menu-club .money'); var text = moneyEl ? moneyEl.innerText : '€'; if (text.includes('€')) return { code: 'EUR', sym: '€' }; if (text.includes('₺')) return { code: 'TRY', sym: '₺' }; if (text.includes('$')) return { code: 'USD', sym: '$' }; if (text.includes('£')) return { code: 'GBP', sym: '£' }; if (text.includes('kr')) return { code: 'SEK', sym: 'kr' }; return { code: 'EUR', sym: '€' }; } // Koordinat dönüşüm (aynı sistem) function statsToLeft(sx, isHome) { return isHome ? Math.round(-0.255800462 * sx + 199.8228530689) : Math.round(0.2555000556 * sx + 8.3741302936); } function statsToTop(sy, isHome) { return isHome ? Math.round(-0.3073207154 * sy + 315.9278777381) : Math.round(0.3070644902 * sy + 9.2794889414); } // Pozisyon rengi: rakip takım her zaman yukarıdan aşağı saldırır // top değerleri 0-315px arasında (pitch yüksekliği) // Küçük top = ekranın üstü = rakibin gol ağı tarafı (defans) // Büyük top = ekranın altı = bizım gol ağı tarafı (forvet) function positionColor(top) { if (top < 105) return '#3182ce'; // Defans - mavi if (top < 210) return '#38a169'; // Orta saha - yeşil return '#e53e3e'; // Forvet - kırmızı } // Rakip overlay ve bilgi kartını temizle function clearRival() { var o = document.getElementById('mz-rival-overlay'); var c = document.getElementById('mz-rival-card'); if (o) o.remove(); if (c) c.remove(); var btn = document.getElementById('mz-rival-btn'); if (btn) { btn.textContent = '🕵️ ' + t('rival'); btn.style.background = '#7b2d8b'; } window._mzRivalIsOpen = false; } window._mzRivalClose = clearRival; // Pitch üzerine overlay çiz function renderOverlay(players, teamData, targetCurrency) { clearRival(); var teamName = teamData.name || 'Rakip'; var pitch = document.getElementById('pitch'); if (!pitch) return; // Overlay doğrudan pitch içine var overlay = document.createElement('div'); overlay.id = 'mz-rival-overlay'; overlay.style.cssText = [ 'position:absolute', 'top:0', 'left:0', 'width:' + pitch.offsetWidth + 'px', 'height:' + pitch.offsetHeight + 'px', 'pointer-events:none', 'z-index:50' ].join(';'); pitch.appendChild(overlay); // Bilgi kartı var card = document.createElement('div'); card.id = 'mz-rival-card'; card.style.cssText = [ 'position:absolute', 'top:0', 'left:0', 'width:fit-content', 'min-width:180px', 'max-width:320px', 'max-height:75vh', 'overflow-y:auto', 'background:#1a202c', 'color:#e2e8f0', 'border:1px solid #4a5568', 'border-radius:8px', 'padding:10px', 'font-size:11px', 'font-family:Segoe UI,sans-serif', 'z-index:99999', 'box-shadow:0 4px 16px rgba(0,0,0,0.5)', 'user-select:none' ].join(';'); // Konumu dinamik ayarla (pitch'in yanına, scroll ile hareket edecek) var fCont = document.getElementById('formation-container'); if (fCont) { var r = fCont.getBoundingClientRect(); card.style.top = (window.scrollY + r.top + 50) + 'px'; card.style.left = (window.scrollX + r.left + 485) + 'px'; } // Header: Logo, İsim, Bayrak var logoUrl = 'https://www.managerzone.com/dynimg/badge.php?team_id=' + teamData.id; var flagUrl = 'https://www.managerzone.com/nocache-910/img/flags/64/' + (teamData.country || 'tr').toLowerCase() + '.png'; var cardHTML = '<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">' + '<img src="' + logoUrl + '" width="24" height="24" style="flex-shrink:0;object-fit:contain">' + '<div style="font-weight:700;font-size:14px;color:#90cdf4;white-space:nowrap">' + teamName + '</div>' + '<img src="' + flagUrl + '" width="16" style="flex-shrink:0;border-radius:2px">' + '</div>'; // Taktik Bilgisi var tacStr = (tacticDisplay[teamData.tactics] || teamData.tactics) + ' / ' + (playstyleDisplay[teamData.playstyle] || teamData.playstyle) + ' / ' + (aggrDisplay[teamData.aggression] || teamData.aggression); cardHTML += '<div style="font-size:10px;color:#a0aec0;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #4a5568">' + tacStr + '</div>'; cardHTML += '<div style="display:flex;gap:4px;margin-bottom:8px;font-size:10px">'; cardHTML += `<span style="color:#ff9800">🟠 ${t('roleGK')}</span>`; cardHTML += `<span style="color:#3182ce">🔵 ${t('roleDef')}</span>`; cardHTML += `<span style="color:#38a169">🟢 ${t('roleMid')}</span>`; cardHTML += `<span style="color:#e53e3e">🔴 ${t('roleFor')}</span>`; cardHTML += '</div>'; // Pozisyona göre sırala: KI (-1), D (1), O (2), F (3) var sortWeight = { 'KI': -1, 'D': 1, 'O': 2, 'F': 3 }; players.sort(function (a, b) { var wA = sortWeight[a.role] === undefined ? 99 : sortWeight[a.role]; var wB = sortWeight[b.role] === undefined ? 99 : sortWeight[b.role]; return wA - wB; }); var totalValue = 0; players.forEach(function (p, i) { if (p.value) totalValue += Number(p.value); var bg = p.col || positionColor(p.top); var role = p.role || (p.top > 210 ? 'F' : (p.top > 105 ? 'O' : 'D')); // Marker var dot = document.createElement('div'); dot.style.cssText = [ 'position:absolute', 'left:' + p.left + 'px', 'top:' + p.top + 'px', 'width:18px', 'height:18px', 'border-radius:50%', 'background:' + bg, 'border:2px solid #fff', 'display:flex', 'align-items:center', 'justify-content:center', 'font-size:9px', 'font-weight:700', 'color:#fff', 'box-shadow:0 2px 6px rgba(0,0,0,0.5)' ].join(';'); dot.textContent = p.shirt || (i + 1); // Pozisyon etiketi (KI/D/O/F) - Küçük balon var label = document.createElement('div'); label.style.cssText = 'position:absolute;top:-8px;right:-8px;background:#fff;color:#000;border-radius:50%;width:10px;height:10px;font-size:7px;display:flex;align-items:center;justify-content:center;border:1px solid #4a5568;font-weight:700'; label.textContent = role; dot.appendChild(label); overlay.appendChild(dot); cardHTML += '<div style="padding:5px 0;border-bottom:1px solid #2d3748;display:flex;align-items:center">'; cardHTML += '<span style="font-size:11px;font-weight:700;color:' + bg + ';width:22px;text-align:right">' + role + '</span>'; cardHTML += '<span style="width:20px;height:20px;border-radius:50%;background:' + bg + ';display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:700;color:#fff;flex-shrink:0;margin-left:10px">' + (p.shirt || (i + 1)) + '</span>'; if (p.age) { cardHTML += '<span style="color:#cbd5e0;font-size:12px;width:38px;margin-left:12px">' + p.age + 'y</span>'; } var valStr = p.value ? targetCurrency.sym + (p.value.toLocaleString()) : '—'; cardHTML += '<span style="color:#cbd5e0;font-size:12px;flex:1;text-align:right">' + valStr + '</span>'; cardHTML += '</div>'; }); // Toplam Değer var totalText = targetCurrency.sym + (totalValue.toLocaleString()); cardHTML += '<div style="margin-top:10px;padding-top:8px;border-top:1px solid #4a5568;font-weight:700;color:#90cdf4;display:flex;justify-content:space-between;font-size:12px">'; cardHTML += '<span>' + t('totalValueLabel') + '</span><span>' + totalText + '</span></div>'; cardHTML += '<button id="mz-rival-close" style="margin-top:10px;width:100%;padding:6px;background:#e53e3e;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:11px;font-weight:700">❌ ' + t('close') + '</button>'; card.innerHTML = cardHTML; document.body.appendChild(card); // --- Draggable (Sürüklenebilir) Mantığı --- (function (el) { var isDrg = false, startX, startY, sL, sT; var header = el.querySelector('div:first-child'); if (header) { header.style.cursor = 'move'; header.title = t('dragToMoveTooltip'); header.addEventListener('mousedown', function (e) { isDrg = true; startX = e.clientX; startY = e.clientY; sL = parseInt(el.style.left) || 0; sT = parseInt(el.style.top) || 0; e.preventDefault(); }); window.addEventListener('mousemove', function (e) { if (!isDrg) return; el.style.left = (sL + e.clientX - startX) + 'px'; el.style.top = (sT + e.clientY - startY) + 'px'; }); window.addEventListener('mouseup', function () { isDrg = false; }); } })(card); document.getElementById('mz-rival-close').addEventListener('click', clearRival); // Formasyonu kaydet (Taktik editöründe kullanılabilir olması için) try { var exportData = { teamName: teamName, players: players.map(p => ({ shirt: p.shirt, left: p.left, top: p.top, role: p.role, age: p.age, value: p.value })) }; localStorage.setItem('mz_scouted_formation', JSON.stringify(exportData)); } catch (e) { } var btn = document.getElementById('mz-rival-btn'); if (btn) { btn.textContent = '❌ ' + t('rival'); btn.style.background = '#c53030'; } window._mzRivalIsOpen = true; } // Takım listesi XML'inden oyuncu detaylarını çek function fetchPlayerDetails(teamData, players, targetCurrInfo, cb) { var url = 'https://www.managerzone.com/xml/team_playerlist.php?sport_id=1&team_id=' + teamData.id; GM_xmlhttpRequest({ method: 'GET', url: url, onload: function (res) { try { var parser = new DOMParser(); var xml = parser.parseFromString(res.responseText, 'text/xml'); var rates = { USD: 1, SEK: 7.4234, EUR: 0.80887, GBP: 0.555957, DKK: 6.00978, NOK: 6.921908, CHF: 1.265201, CAD: 1.3003, AUD: 1.309244, "R$": 2.827003, BRL: 2.827003, ILS: 4.378812, MXN: 10.82507, ARS: 2.807162, BOB: 7.905644, PYG: 5671.047, UYU: 28.88898, RUB: 28.21191, PLN: 3.801452, ISK: 71.15307, BGN: 1.576971, ZAR: 5.999531, THB: 43.46507, SIT: 190.539, SKK: 29.75788, JPY: 123.7233, INR: 43.66706, MZ: 7.4234, MM: 7.4234, "点": 7.4234, TRY: 7.4234 }; var tp = xml.querySelector('TeamPlayers'); var sourceCurr = (tp ? tp.getAttribute('teamCurrency') : null) || xml.documentElement.getAttribute('currency') || 'USD'; var sourceRate = rates[sourceCurr] || 1; var targetRate = rates[targetCurrInfo.code] || 1; players.forEach(function (p) { var node = xml.querySelector('Player[id="' + p.id + '"]'); if (node) { p.age = node.getAttribute('age'); p.height = node.getAttribute('height'); p.country = node.getAttribute('countryShortname'); var v = Number(node.getAttribute('value')); if (!isNaN(v)) { p.value = Math.round(v / sourceRate * targetRate); } else { p.value = 0; } } }); } catch (e) { console.error("MZ Advanced XML Parse Error:", e); } cb(players, teamData, targetCurrInfo); }, onerror: function () { cb(players, teamData, targetCurrInfo); } }); } // Maç ID ile stats XML.gz indir → decompress → parse function loadMatchById(matchId, isRetry) { var btn = document.getElementById('mz-rival-btn'); if (btn) btn.textContent = '⏳ ' + t('loading'); // İki kaynak: 1. Ana sunucu (Daha hızlı), 2. Arşiv (download06) var urls = [ 'https://www.managerzone.com/matchviewer/getMatchFiles.php?type=stats&mid=' + matchId + '&sport=soccer', 'https://download06.managerzone.com/data/soccer/' + matchId.slice(-1) + '/' + matchId.charAt(matchId.length - 2) + '/stats' + matchId + '.xml.gz' ]; function tryFetch(idx) { if (idx >= urls.length) { if (!isRetry) { showToast(t('rivalDataRequest'), '#f39c12'); forceProcessMatch(matchId, function () { loadMatchById(matchId, true); }); } else { showToast(t('rivalLoadError'), '#e53e3e'); if (btn) { btn.textContent = '🕵️ ' + t('rival'); btn.style.background = '#7b2d8b'; } } return; } GM_xmlhttpRequest({ method: 'GET', url: urls[idx], responseType: 'arraybuffer', onload: function (res) { if (res.status !== 200 || !res.response || res.response.byteLength < 50) { return tryFetch(idx + 1); } try { var data = new Uint8Array(res.response); // Eğer GZIP ise decompress et, değilse direkt oku var text; if (data[0] === 0x1f && data[1] === 0x8b) { text = pako.ungzip(data, { to: 'string' }); } else { text = new TextDecoder().decode(data); } var parser = new DOMParser(); var xml = parser.parseFromString(text, 'text/xml'); var teams = xml.documentElement.getElementsByTagName('Team'); if (!teams.length) return tryFetch(idx + 1); showTeamSelectionModal(xml, teams, matchId); } catch (e) { console.error("Parse error:", e); tryFetch(idx + 1); } }, onerror: function () { tryFetch(idx + 1); } }); } tryFetch(0); } // Arka planda sunucuyu veri üretmeye zorlar (AJAX Trigger) function forceProcessMatch(matchId, cb, tryCount) { tryCount = tryCount || 1; if (tryCount > 4) { if (cb) cb(); return; } var checkUrl = 'https://www.managerzone.com/ajax.php?p=matchViewer&sub=check-match&type=2d&mid=' + matchId + '&sport=soccer'; GM_xmlhttpRequest({ method: 'GET', url: checkUrl, headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }, onload: function (resp) { var res = {}; try { res = JSON.parse(resp.responseText); } catch (e) { } if (res.response === 'ok') { // Veri hazır, yarım saniye bekle ve asıl XML'i çek setTimeout(cb, 500); } else if (res.response === 'queued') { // Sırada, 1.5 saniye sonra tekrar kontrol et setTimeout(function () { forceProcessMatch(matchId, cb, tryCount + 1); }, 1500); } else { // Hata veya veri yok if (cb) cb(); } }, onerror: function () { if (cb) cb(); } }); } // Takım seçim modalı (confirm() yerine styled UI) function showTeamSelectionModal(xml, teams, matchId) { var existing = document.getElementById('mz-rival-team-modal'); if (existing) existing.remove(); var modal = document.createElement('div'); modal.id = 'mz-rival-team-modal'; modal.style.cssText = [ 'position:fixed', 'top:50%', 'left:50%', 'transform:translate(-50%,-50%)', 'background:#1a202c', 'color:#e2e8f0', 'border:1px solid #4a5568', 'border-radius:10px', 'padding:20px 24px', 'z-index:999999', 'font-family:Segoe UI,sans-serif', 'min-width:280px', 'box-shadow:0 8px 32px rgba(0,0,0,0.7)' ].join(';'); var t0 = teams[0].getAttribute('name'); var t1 = teams[1] ? teams[1].getAttribute('name') : ''; modal.innerHTML = '<div style="font-weight:700;font-size:14px;margin-bottom:14px;color:#90cdf4">' + t('rivalMatchTitle') + '</div>' + '<div style="font-size:12px;margin-bottom:12px;color:#a0aec0">' + t('matchId') + ': ' + matchId + '</div>' + '<button id="mz-team-0" style="display:block;width:100%;padding:8px;margin-bottom:8px;background:#2d3748;color:#e2e8f0;border:1px solid #4a5568;border-radius:6px;cursor:pointer;font-size:13px;text-align:left">' + t0 + '</button>' + (t1 ? '<button id="mz-team-1" style="display:block;width:100%;padding:8px;margin-bottom:8px;background:#2d3748;color:#e2e8f0;border:1px solid #4a5568;border-radius:6px;cursor:pointer;font-size:13px;text-align:left">' + t1 + '</button>' : '') + '<button id="mz-team-cancel" style="display:block;width:100%;padding:6px;background:#4a5568;color:#e2e8f0;border:none;border-radius:6px;cursor:pointer;font-size:12px">' + t('cancel') + '</button>'; document.body.appendChild(modal); function selectTeam(idx) { modal.remove(); var isHome = (idx === 0); var rivalTeam = teams[idx]; var teamId = rivalTeam.getAttribute('id'); var teamName = rivalTeam.getAttribute('name'); var currInfo = getUserCurrencyInfo(); var playersXml = xml.querySelectorAll('Player[teamId="' + teamId + '"][origin*=","]'); var players = []; // Tüm rakip takımın ortalama dikey konumuna bakarak formasyonun yönünü belirliyoruz. var totalTy = 0; playersXml.forEach(function (p) { totalTy += statsToTop(Number(p.getAttribute('origin').split(',')[1]), isHome); }); var teamNeedsFlip = (totalTy / playersXml.length) > 160; playersXml.forEach(function (p) { var origin = p.getAttribute('origin').split(','); var sx_raw = Number(origin[0]); var sy_raw = Number(origin[1]); var tx = statsToLeft(sx_raw, isHome); var ty = statsToTop(sy_raw, isHome); /** * YÖN VE KOORDİNAT HİZALAMA: * Takım eğer alttaysa (teamNeedsFlip), 180 derece çevirip yukarı (Goal tarafına) alıyoruz. */ var finalTop = teamNeedsFlip ? (318.5 - ty) : (ty - 6.5); var finalLeft = 201 - tx; players.push({ id: p.getAttribute('id'), shirt: p.getAttribute('shirtno'), left: finalLeft, top: finalTop }); }); var teamData = { id: teamId, name: teamName, country: rivalTeam.getAttribute('country'), tactics: rivalTeam.getAttribute('tactic') || rivalTeam.getAttribute('tactics'), playstyle: rivalTeam.getAttribute('playstyle'), aggression: rivalTeam.getAttribute('aggression') }; // Gerçek Kaleciyi Bul... if (players.length > 0) { var gk = players.reduce(function (prev, curr) { var prevDist = Math.abs(prev.left - 100); var currDist = Math.abs(curr.left - 100); // Hem en üstte olmalı hem de kale merkezine yakın if (curr.top < prev.top - 10) return curr; if (curr.top < prev.top + 10 && currDist < prevDist) return curr; return prev; }); players.forEach(function (p) { if (p === gk) { p.role = 'KI'; p.col = '#ff9800'; // Turuncu (Kaleci için ayrı renk) } else { p.role = (p.top > 210 ? 'F' : (p.top > 105 ? 'O' : 'D')); p.col = positionColor(p.top); } }); } // Formasyonu kaydet (Hızlı erişim için) try { var exportData = { teamName: teamName, players: players.map(p => ({ shirt: p.shirt, left: p.left, top: p.top, role: p.role })) }; localStorage.setItem('mz_scouted_formation', JSON.stringify(exportData)); } catch (e) { } fetchPlayerDetails(teamData, players, currInfo, renderOverlay); } document.getElementById('mz-team-0').addEventListener('click', function () { selectTeam(0); }); if (t1) document.getElementById('mz-team-1').addEventListener('click', function () { selectTeam(1); }); document.getElementById('mz-team-cancel').addEventListener('click', function () { modal.remove(); var btn = document.getElementById('mz-rival-btn'); if (btn) { btn.textContent = '🕵️ Rakip'; btn.style.background = '#7b2d8b'; } }); } // Match ID giriş modalı function showMatchIdModal() { var existing = document.getElementById('mz-rival-modal'); if (existing) existing.remove(); var modal = document.createElement('div'); modal.id = 'mz-rival-modal'; modal.style.cssText = [ 'position:fixed', 'top:50%', 'left:50%', 'transform:translate(-50%,-50%)', 'background:#1a202c', 'color:#e2e8f0', 'border:1px solid #4a5568', 'border-radius:10px', 'padding:20px 24px', 'z-index:999999', 'font-family:Segoe UI,sans-serif', 'min-width:300px', 'box-shadow:0 8px 32px rgba(0,0,0,0.7)' ].join(';'); modal.innerHTML = `<div style="font-weight:700;font-size:14px;margin-bottom:14px;color:#90cdf4">${t('rivalFormationTitle')}</div>` + `<div style="font-size:12px;margin-bottom:10px;color:#a0aec0">${t('matchIdInstruction')}</div>` + `<input id="mz-rival-match-input" type="text" placeholder="${t('matchIdExample')}" style="width:100%;box-sizing:border-box;padding:8px;border-radius:6px;border:1px solid #4a5568;background:#2d3748;color:#e2e8f0;font-size:13px;margin-bottom:12px">` + '<div style="display:flex;gap:8px">' + `<button id="mz-rival-load-btn" style="flex:1;padding:8px;background:#7b2d8b;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:600">${t('load')}</button>` + `<button id="mz-rival-modal-cancel" style="padding:8px 14px;background:#4a5568;color:#e2e8f0;border:none;border-radius:6px;cursor:pointer;font-size:13px">${t('cancel')}</button>` + '</div>'; document.body.appendChild(modal); var input = document.getElementById('mz-rival-match-input'); input.focus(); function doLoad() { var mid = input.value.trim(); if (!mid || isNaN(mid) || mid.length < 5 || mid.length > 14) { input.style.border = '1px solid #e53e3e'; return; } modal.remove(); loadMatchById(mid); } document.getElementById('mz-rival-load-btn').addEventListener('click', doLoad); document.getElementById('mz-rival-modal-cancel').addEventListener('click', function () { modal.remove(); }); input.addEventListener('keydown', function (e) { if (e.key === 'Enter') doLoad(); }); } // Global değişkenler window._mzRivalIsOpen = false; window._mzRivalShowModal = showMatchIdModal; window._mzRivalClose = clearRival; })(); } /* === RESIDENCY & TRANSFER INFO MODULE === */ async function initializeResidencyModule() { if (!Core.settings.residencyInfo) return; const parseMzDate = (dateString) => { const parts = dateString.split('-'); if (parts.length < 3) return new Date(0); const [day, month, year] = parts.map(Number); return new Date(year, month - 1, day); }; const fetchHtml = async (url) => { try { const response = await fetch(url); const text = await response.text(); return new DOMParser().parseFromString(text, "text/html"); } catch (error) { console.warn("[MZone Advanced] Residency Fetch error:", error); return null; } }; const extractResidency = (doc) => { if (!doc) return { days: 0, price: '' }; const transfers = doc.querySelector("div.baz > div > div.win_back > table.hitlist"); const history = transfers?.querySelector("tbody"); if (history && history.children.length > 1) { const lastRow = history.lastElementChild; const arrived = lastRow.querySelector("td")?.innerText.trim(); if (!arrived) return { days: -1, price: '' }; const days = Math.floor((new Date() - parseMzDate(arrived)) / 86400000); const price = lastRow.querySelector("td:last-child")?.innerText.trim(); const currency = transfers.querySelector("thead tr td:last-child")?.innerText?.match(/.*\((.*)\)/)?.[1] || ""; return { days, price: price + ' ' + currency }; } return { days: -1, price: '' }; }; const injectUI = (container, data) => { if (container.querySelector(".mz-residency-row")) return; const urlParams = new URLSearchParams(window.location.search); const p = urlParams.get('p'); // Kadro ve Profil sayfalarında (p=players) boşluk daha fazla olduğu için negatif marjin kullanıyoruz. let marginStyle = (p === 'players') ? "margin: 5px 0 5px 0;" : "margin: 5px 0;"; // Antrenman Kampı (TC) Paketi varsa: Sağda buton bulunduğu için kutuyu daraltıp içeriği sola kaydır const hasTCPackage = container.querySelector(".player_tc_package_information"); if (p === 'players' && hasTCPackage) { marginStyle = "margin: 5px 0px 5px 0;"; } const flexAlign = hasTCPackage ? "center" : "center"; const row = document.createElement("div"); row.className = "mz-residency-row"; row.style.cssText = `background: linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 100%); border: 1px solid #7dd3fc; border-radius: 6px; padding: 8px 15px; ${marginStyle} font-size: 13px; display: flex; gap: 25px; align-items: center; justify-content: ${flexAlign}; color: #1d4ed8; font-family: 'Inter', sans-serif; box-shadow: 0 2px 4px rgba(0,0,0,0.05); clear: both; transition: all 0.2s ease;`; let content = ""; const residencyLabel = Core.i18n.getText('residencyDays'); const feeLabel = Core.i18n.getText('transferFee'); const careerLabel = Core.i18n.getText('entireCareer'); if (data.days >= 0) { const dayText = data.days === 1243 ? careerLabel : `≤ ${data.days}`; content += `<span>📅 ${residencyLabel}: <b style="color: #059669;">${dayText}</b></span>`; if (data.price) { content += `<span>💰 ${feeLabel}: <b style="color: #e11d48;">${data.price}</b></span>`; } } else { content += `<span>📅 ${residencyLabel}: <b style="color: #059669;">${careerLabel}</b></span>`; } row.innerHTML = content; // Enjeksiyon: Transfer sayfasında .mainContent, profilde .win_back kullanılır. const target = container.querySelector(".mainContent") || container.querySelector(".win_back") || container; target.appendChild(row); }; const processedContainers = new Set(); const processPlayer = async (container) => { if (processedContainers.has(container)) return; processedContainers.add(container); let pid = null; const idSpan = container.querySelector(".player_id_span"); if (idSpan) { pid = idSpan.innerText.replace(/\D/g, '').trim(); } if (!pid) { const link = container.querySelector('a[href*="pid="]'); if (link) { const match = link.href.match(/pid=(\d+)/); if (match) pid = match[1]; } } if (pid) { const url = `https://${window.location.hostname}/?p=players&pid=${pid}&sub=history`; const doc = await fetchHtml(url); if (doc) { const data = extractResidency(doc); injectUI(container, data); } } }; const scanAndProcess = async () => { const containers = document.querySelectorAll(".playerContainer"); const currentHash = containers.length; if (window._lastResidencyHash === currentHash) return; window._lastResidencyHash = currentHash; for (const container of containers) { await processPlayer(container); } }; // İlk tarama await scanAndProcess(); // Dinamik yüklemeler için setInterval (JANK önleyici) setInterval(async () => { await scanAndProcess(); }, 1500); } // Initialize scripts // disabled: tactic writes require a dedicated preview and confirmation // initialized only by the route-scoped module })(); // ========================================== // MZ Aging Deterioration Tracker (Integrated) // ========================================== (function () { 'use strict'; // Bellek tablosu - bir oyuncuyu saniyede birden fazla kez işlememek için const processedPlayers = {}; // MZ Futbol Yetenek Indeksleri (Backend graph karşılıkları) const SKILL_SPEED = 2; const SKILL_STAMINA = 3; // Gerileme Formülü function getDropPercentage(text) { let t = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; // bar_neg_(seviye) ve top düşüşü(_ball veya _puck) ibarelerini tarar let r2 = /bar_neg_(\d+)(_ball|_puck)?/g; let m2; while ((m2 = r2.exec(text)) !== null) { // Eğer top kaybı yaşandıysa (_ball varsa), hesaplamayı BAŞA SAR. // Çünkü oyuncu bir alt topa düştü ve yüzdesi sıfırlandı. if (m2[2]) { t = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; } let level = parseInt(m2[1]); if (level >= 1 && level <= 10) { t[level]++; } } // Güncel top kaybına doğru giden süreçteki gerileme eksi yüzdesi let ret = t[1] * 0.645 * 1 + t[2] * 0.55 * 2 + t[3] * 0.7 * 3 + t[4] * 0.85 * 4 + t[5] * 0.96 * 5 + t[6] * 1.111 * 6 + t[7] * 1.3 * 7 + t[8] * 1.6 * 8 + t[9] * 2.02 * 9 + t[10] * 2.4 * 10; // MAKSİMUM SINIR: Oyuncu en fazla 2 top kaybedebildiği için // 3. bir top kaybı ihtimali kalmamış bile olsa matematiksel birikim -99.9%'u geçmemeli. if (ret > 99.9) { ret = 99.9; } return parseFloat(ret); } async function processPlayerDrop(pid, DOMScope) { if (processedPlayers[pid]) return; // Zaten işleme konulduysa tekrar çekme processedPlayers[pid] = true; try { // Hız Grafiği (Speed) let speedRes = await fetch(`/ajax.php?p=trainingGraph&sub=getJsonTrainingHistory&sport=soccer&player_id=${pid}&skill_id=${SKILL_SPEED}`); let speedText = await speedRes.text(); let speedDrop = getDropPercentage(speedText); // Dayanıklılık Grafiği (Stamina) let staminaRes = await fetch(`/ajax.php?p=trainingGraph&sub=getJsonTrainingHistory&sport=soccer&player_id=${pid}&skill_id=${SKILL_STAMINA}`); let staminaText = await staminaRes.text(); let staminaDrop = getDropPercentage(staminaText); // Değerler sıfırdan büyükse HTML Enjeksiyonu Yap if (speedDrop > 0) injectDropBadge(DOMScope, 0, speedDrop); // 0th Skill = Hız if (staminaDrop > 0) injectDropBadge(DOMScope, 1, staminaDrop); // 1st Skill = Dayanıklılık } catch (e) { console.error("MZ Aging Deterioration Fetch error: ", e); processedPlayers[pid] = false; // Hata verirse ileride tekrar deneyebilir } } // Dışarıdan tetiklenebilmesi için (Örn: yenileme butonuna tıklandığında) window.mzAgingRefresh = function (pid, container) { delete processedPlayers[pid]; if (container) { delete container.dataset.deteriorationChecked; } processPlayerDrop(pid, container); }; function injectDropBadge(DOMScope, skillOffset, dropVal) { if (dropVal <= 0.0) return; // Orijinal topların veya sayının bulunduğu kapsayıcıyı ara let skillImgs = DOMScope.querySelectorAll("img.skill, div.skilladd"); if (skillImgs.length == 0) return; let skillContainer = skillImgs[skillOffset]; if (!skillContainer) return; let row = skillContainer.closest("tr, li"); if (!row) row = skillContainer.parentNode; // Scriptin mükerrer eklemesini engelle veya // Orijinal van.mz scripti zaten yüzde basmışsa çakışmamak için çık let existingText = row.innerText || ""; if (row.querySelector(".mz-aging-deterioration") || (existingText.includes('%') && existingText.includes('-'))) { return; } // van.mz tarzında gri, sade eksi değer etiketini oluştur let badge = document.createElement("span"); badge.style.color = "#666"; // Orijinal van.mz gri tonu badge.style.fontWeight = "normal"; badge.style.fontSize = "11px"; badge.className = "mz-aging-deterioration skill_exact_van"; badge.textContent = `-${dropVal.toFixed(1)}%`; badge.title = "Yaşlılık Kaynaklı Antrenman Düşüşü (Gerileme)"; // Sayfa türüne göre dinamik konumlandırma (Overlap ve Kayma engelleyici) const isProfilePage = window.location.search.includes('p=players') && (window.location.search.includes('pid=') || window.location.search.includes('sub=profile')); const isRosterPage = window.location.search.includes('p=players') && !window.location.search.includes('pid=') && !window.location.search.includes('sub=profile'); if (isProfilePage || isRosterPage) { badge.style.position = "absolute"; badge.style.left = "100%"; badge.style.top = "0"; badge.style.marginLeft = "16px"; badge.style.whiteSpace = "nowrap"; } else { badge.style.position = "absolute"; badge.style.left = "100%"; badge.style.top = "0"; badge.style.marginLeft = "6px"; badge.style.whiteSpace = "nowrap"; } // Yüzde veya top sahasının sonuna append et ( (7) yazısının hemen yanı gibi) let valueSpan = row.querySelector(".skillval, .responsive-hide, .skill_exact_van"); if (valueSpan) { valueSpan.style.position = "relative"; // Mutlak konumun düzgün çalışması için geri eklendi valueSpan.appendChild(badge); } else { // Eğer skillval bulunamazsa top objesinin hemen yanına iliştir let imgParent = skillContainer.parentNode; imgParent.style.position = "relative"; imgParent.appendChild(badge); } } function scanForPlayers() { // MZ Sayfalarındaki Muhtemel Oyuncu Blokları (Transfer, Taktik, Profil vs.) let containers = document.querySelectorAll(".playerContainer, .player_info, #playerProfile, tr"); containers.forEach(p => { if (p.dataset.deteriorationChecked) return; // Player ID bul let htmlMatch = p.innerHTML.match(/player_id_(\d+)/) || p.innerHTML.match(/pid=(\d+)/); if (htmlMatch) { let pid = htmlMatch[1]; // Gereksiz Taramayı Engellenek İçin Optimise (Sadece Yaş Saptanıyorsa Kontrol Et) let ageMatch = p.innerText.match(/(Age|Yaş|Edad)[:\n\s]*(\d+)/i); if (ageMatch && parseInt(ageMatch[2]) < 28) { p.dataset.deteriorationChecked = "true"; return; // 28 yaşından küçükler genelde bozulmaz, AJAX yüklemeye gerek yok } p.dataset.deteriorationChecked = "true"; processPlayerDrop(pid, p); } }); } // MZ İçinde Sekmeler Değiştiğinde Yakalayabilmek İçin Döngü // Artık 'TrainingModule' gerilemeyi otomatik hesapladığı için eski hantal tarayıcı kapatıldı. // setInterval(scanForPlayers, 2000); // scanForPlayers(); // --- MOBİL UI YÖNETİCİSİ (BAŞLANGIÇTA KAPALI GELME ÖZELLİĞİ) --- const mzIsMobile = (function () { if (navigator.userAgentData && navigator.userAgentData.mobile) return true; const ua = (navigator.userAgent || navigator.vendor || window.opera).toLowerCase(); return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(ua) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(ua.substr(0, 4)); })(); if (mzIsMobile) { const setupMobileLauncher = () => { const $quickMenu = $('#mz-quick-menu-container'); const $globalInfo = $('#mz-global-info-btn'); if ($quickMenu.length > 0 || $globalInfo.length > 0) { // 1. Başlangıçta ikonları gizle if($quickMenu.length > 0) $quickMenu.addClass('mz-mobile-stashed'); if($globalInfo.length > 0) $globalInfo.addClass('mz-mobile-stashed'); // 2. Master Launcher Butonunu Oluştur if ($('#mz-mobile-master-launcher').length === 0) { const $launcher = $('<div id="mz-mobile-master-launcher">MZ</div>'); $('body').append($launcher); $launcher.on('click', function() { const isHidden = $('#mz-quick-menu-container').hasClass('mz-mobile-stashed') || $('#mz-global-info-btn').hasClass('mz-mobile-stashed'); if (isHidden) { $('#mz-quick-menu-container').removeClass('mz-mobile-stashed'); $('#mz-global-info-btn').removeClass('mz-mobile-stashed'); $(this).text('×').css('background', '#dc3545'); // Kapatma simgesi } else { $('#mz-quick-menu-container').addClass('mz-mobile-stashed'); $('#mz-global-info-btn').addClass('mz-mobile-stashed'); $(this).text('MZ').css('background', '#00529B'); // Geri açma simgesi } }); } } else { // İkonlar render edilene kadar periyodik kontrol setTimeout(setupMobileLauncher, 1000); } }; setupMobileLauncher(); } })(); }).call(window, __mzcPako2, __mzcDexie4, __mzcJQuery, __mzcJQuery); }); /* mzcopilot:module:[email protected] */ window.__mzcMega.run("tactics-manager", "15.0.4", function () { (function ($, jQuery) { (function () { 'use strict'; !function (r, t) { "object" == typeof exports && "undefined" != typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define(t) : (r = "undefined" != typeof globalThis ? globalThis : r || self).jsSHA = t() }(this, (function () { "use strict"; var r = function (t, n) { return r = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function (r, t) { r.__proto__ = t } || function (r, t) { for (var n in t) Object.prototype.hasOwnProperty.call(t, n) && (r[n] = t[n]) }, r(t, n) }; "function" == typeof SuppressedError && SuppressedError; var t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", n = "ARRAYBUFFER not supported by this environment", i = "UINT8ARRAY not supported by this environment"; function e(r, t, n, i) { var e, o, u, s = t || [0], f = (n = n || 0) >>> 3, h = -1 === i ? 3 : 0; for (e = 0; e < r.length; e += 1)o = (u = e + f) >>> 2, s.length <= o && s.push(0), s[o] |= r[e] << 8 * (h + i * (u % 4)); return { value: s, binLen: 8 * r.length + n } } function o(r, o, u) { switch (o) { case "UTF8": case "UTF16BE": case "UTF16LE": break; default: throw new Error("encoding must be UTF8, UTF16BE, or UTF16LE") }switch (r) { case "HEX": return function (r, t, n) { return function (r, t, n, i) { var e, o, u, s; if (0 != r.length % 2) throw new Error("String of HEX type must be in byte increments"); var f = t || [0], h = (n = n || 0) >>> 3, a = -1 === i ? 3 : 0; for (e = 0; e < r.length; e += 2) { if (o = parseInt(r.substr(e, 2), 16), isNaN(o)) throw new Error("String of HEX type contains invalid characters"); for (u = (s = (e >>> 1) + h) >>> 2; f.length <= u;)f.push(0); f[u] |= o << 8 * (a + i * (s % 4)) } return { value: f, binLen: 4 * r.length + n } }(r, t, n, u) }; case "TEXT": return function (r, t, n) { return function (r, t, n, i, e) { var o, u, s, f, h, a, c, w, E = 0, v = n || [0], l = (i = i || 0) >>> 3; if ("UTF8" === t) for (c = -1 === e ? 3 : 0, s = 0; s < r.length; s += 1)for (u = [], 128 > (o = r.charCodeAt(s)) ? u.push(o) : 2048 > o ? (u.push(192 | o >>> 6), u.push(128 | 63 & o)) : 55296 > o || 57344 <= o ? u.push(224 | o >>> 12, 128 | o >>> 6 & 63, 128 | 63 & o) : (s += 1, o = 65536 + ((1023 & o) << 10 | 1023 & r.charCodeAt(s)), u.push(240 | o >>> 18, 128 | o >>> 12 & 63, 128 | o >>> 6 & 63, 128 | 63 & o)), f = 0; f < u.length; f += 1) { for (h = (a = E + l) >>> 2; v.length <= h;)v.push(0); v[h] |= u[f] << 8 * (c + e * (a % 4)), E += 1 } else for (c = -1 === e ? 2 : 0, w = "UTF16LE" === t && 1 !== e || "UTF16LE" !== t && 1 === e, s = 0; s < r.length; s += 1) { for (o = r.charCodeAt(s), !0 === w && (o = (f = 255 & o) << 8 | o >>> 8), h = (a = E + l) >>> 2; v.length <= h;)v.push(0); v[h] |= o << 8 * (c + e * (a % 4)), E += 2 } return { value: v, binLen: 8 * E + i } }(r, o, t, n, u) }; case "B64": return function (r, n, i) { return function (r, n, i, e) { var o, u, s, f, h, a, c = 0, w = n || [0], E = (i = i || 0) >>> 3, v = -1 === e ? 3 : 0, l = r.indexOf("="); if (-1 === r.search(/^[a-zA-Z0-9=+/]+$/)) throw new Error("Invalid character in base-64 string"); if (r = r.replace(/=/g, ""), -1 !== l && l < r.length) throw new Error("Invalid '=' found in base-64 string"); for (o = 0; o < r.length; o += 4) { for (f = r.substr(o, 4), s = 0, u = 0; u < f.length; u += 1)s |= t.indexOf(f.charAt(u)) << 18 - 6 * u; for (u = 0; u < f.length - 1; u += 1) { for (h = (a = c + E) >>> 2; w.length <= h;)w.push(0); w[h] |= (s >>> 16 - 8 * u & 255) << 8 * (v + e * (a % 4)), c += 1 } } return { value: w, binLen: 8 * c + i } }(r, n, i, u) }; case "BYTES": return function (r, t, n) { return function (r, t, n, i) { var e, o, u, s, f = t || [0], h = (n = n || 0) >>> 3, a = -1 === i ? 3 : 0; for (o = 0; o < r.length; o += 1)e = r.charCodeAt(o), u = (s = o + h) >>> 2, f.length <= u && f.push(0), f[u] |= e << 8 * (a + i * (s % 4)); return { value: f, binLen: 8 * r.length + n } }(r, t, n, u) }; case "ARRAYBUFFER": try { new ArrayBuffer(0) } catch (r) { throw new Error(n) } return function (r, t, n) { return function (r, t, n, i) { return e(new Uint8Array(r), t, n, i) }(r, t, n, u) }; case "UINT8ARRAY": try { new Uint8Array(0) } catch (r) { throw new Error(i) } return function (r, t, n) { return e(r, t, n, u) }; default: throw new Error("format must be HEX, TEXT, B64, BYTES, ARRAYBUFFER, or UINT8ARRAY") } } function u(r, e, o, u) { switch (r) { case "HEX": return function (r) { return function (r, t, n, i) { var e, o, u = "0123456789abcdef", s = "", f = t / 8, h = -1 === n ? 3 : 0; for (e = 0; e < f; e += 1)o = r[e >>> 2] >>> 8 * (h + n * (e % 4)), s += u.charAt(o >>> 4 & 15) + u.charAt(15 & o); return i.outputUpper ? s.toUpperCase() : s }(r, e, o, u) }; case "B64": return function (r) { return function (r, n, i, e) { var o, u, s, f, h, a = "", c = n / 8, w = -1 === i ? 3 : 0; for (o = 0; o < c; o += 3)for (f = o + 1 < c ? r[o + 1 >>> 2] : 0, h = o + 2 < c ? r[o + 2 >>> 2] : 0, s = (r[o >>> 2] >>> 8 * (w + i * (o % 4)) & 255) << 16 | (f >>> 8 * (w + i * ((o + 1) % 4)) & 255) << 8 | h >>> 8 * (w + i * ((o + 2) % 4)) & 255, u = 0; u < 4; u += 1)a += 8 * o + 6 * u <= n ? t.charAt(s >>> 6 * (3 - u) & 63) : e.b64Pad; return a }(r, e, o, u) }; case "BYTES": return function (r) { return function (r, t, n) { var i, e, o = "", u = t / 8, s = -1 === n ? 3 : 0; for (i = 0; i < u; i += 1)e = r[i >>> 2] >>> 8 * (s + n * (i % 4)) & 255, o += String.fromCharCode(e); return o }(r, e, o) }; case "ARRAYBUFFER": try { new ArrayBuffer(0) } catch (r) { throw new Error(n) } return function (r) { return function (r, t, n) { var i, e = t / 8, o = new ArrayBuffer(e), u = new Uint8Array(o), s = -1 === n ? 3 : 0; for (i = 0; i < e; i += 1)u[i] = r[i >>> 2] >>> 8 * (s + n * (i % 4)) & 255; return o }(r, e, o) }; case "UINT8ARRAY": try { new Uint8Array(0) } catch (r) { throw new Error(i) } return function (r) { return function (r, t, n) { var i, e = t / 8, o = -1 === n ? 3 : 0, u = new Uint8Array(e); for (i = 0; i < e; i += 1)u[i] = r[i >>> 2] >>> 8 * (o + n * (i % 4)) & 255; return u }(r, e, o) }; default: throw new Error("format must be HEX, B64, BYTES, ARRAYBUFFER, or UINT8ARRAY") } } var s = [1116352408, 1899447441, 3049323471, 3921009573, 961987163, 1508970993, 2453635748, 2870763221, 3624381080, 310598401, 607225278, 1426881987, 1925078388, 2162078206, 2614888103, 3248222580, 3835390401, 4022224774, 264347078, 604807628, 770255983, 1249150122, 1555081692, 1996064986, 2554220882, 2821834349, 2952996808, 3210313671, 3336571891, 3584528711, 113926993, 338241895, 666307205, 773529912, 1294757372, 1396182291, 1695183700, 1986661051, 2177026350, 2456956037, 2730485921, 2820302411, 3259730800, 3345764771, 3516065817, 3600352804, 4094571909, 275423344, 430227734, 506948616, 659060556, 883997877, 958139571, 1322822218, 1537002063, 1747873779, 1955562222, 2024104815, 2227730452, 2361852424, 2428436474, 2756734187, 3204031479, 3329325298], f = [3238371032, 914150663, 812702999, 4144912697, 4290775857, 1750603025, 1694076839, 3204075428], h = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]; function a(r) { var t = { outputUpper: !1, b64Pad: "=", outputLen: -1 }, n = r || {}, i = "Output length must be a multiple of 8"; if (t.outputUpper = n.outputUpper || !1, n.b64Pad && (t.b64Pad = n.b64Pad), n.outputLen) { if (n.outputLen % 8 != 0) throw new Error(i); t.outputLen = n.outputLen } else if (n.shakeLen) { if (n.shakeLen % 8 != 0) throw new Error(i); t.outputLen = n.shakeLen } if ("boolean" != typeof t.outputUpper) throw new Error("Invalid outputUpper formatting option"); if ("string" != typeof t.b64Pad) throw new Error("Invalid b64Pad formatting option"); return t } function c(r, t) { return r >>> t | r << 32 - t } function w(r, t) { return r >>> t } function E(r, t, n) { return r & t ^ ~r & n } function v(r, t, n) { return r & t ^ r & n ^ t & n } function l(r) { return c(r, 2) ^ c(r, 13) ^ c(r, 22) } function p(r, t) { var n = (65535 & r) + (65535 & t); return (65535 & (r >>> 16) + (t >>> 16) + (n >>> 16)) << 16 | 65535 & n } function A(r, t, n, i) { var e = (65535 & r) + (65535 & t) + (65535 & n) + (65535 & i); return (65535 & (r >>> 16) + (t >>> 16) + (n >>> 16) + (i >>> 16) + (e >>> 16)) << 16 | 65535 & e } function d(r, t, n, i, e) { var o = (65535 & r) + (65535 & t) + (65535 & n) + (65535 & i) + (65535 & e); return (65535 & (r >>> 16) + (t >>> 16) + (n >>> 16) + (i >>> 16) + (e >>> 16) + (o >>> 16)) << 16 | 65535 & o } function y(r) { return c(r, 7) ^ c(r, 18) ^ w(r, 3) } function U(r) { return c(r, 6) ^ c(r, 11) ^ c(r, 25) } function T(r) { return "SHA-224" == r ? f.slice() : h.slice() } function b(r, t) { var n, i, e, o, u, f, h, a, T, b, R, m, F = []; for (n = t[0], i = t[1], e = t[2], o = t[3], u = t[4], f = t[5], h = t[6], a = t[7], R = 0; R < 64; R += 1)F[R] = R < 16 ? r[R] : A(c(m = F[R - 2], 17) ^ c(m, 19) ^ w(m, 10), F[R - 7], y(F[R - 15]), F[R - 16]), T = d(a, U(u), E(u, f, h), s[R], F[R]), b = p(l(n), v(n, i, e)), a = h, h = f, f = u, u = p(o, T), o = e, e = i, i = n, n = p(T, b); return t[0] = p(n, t[0]), t[1] = p(i, t[1]), t[2] = p(e, t[2]), t[3] = p(o, t[3]), t[4] = p(u, t[4]), t[5] = p(f, t[5]), t[6] = p(h, t[6]), t[7] = p(a, t[7]), t } return function (t) { function n(r, n, i) { var e = this; if ("SHA-224" !== r && "SHA-256" !== r) throw new Error("Chosen SHA variant is not supported"); var u = i || {}; return (e = t.call(this, r, n, i) || this).t = e.i, e.o = !0, e.u = -1, e.h = o(e.v, e.l, e.u), e.p = b, e.A = function (r) { return r.slice() }, e.U = T, e.T = function (t, n, i, e) { return function (r, t, n, i, e) { for (var o, u = 15 + (t + 65 >>> 9 << 4), s = t + n; r.length <= u;)r.push(0); for (r[t >>> 5] |= 128 << 24 - t % 32, r[u] = 4294967295 & s, r[u - 1] = s / 4294967296 | 0, o = 0; o < r.length; o += 16)i = b(r.slice(o, o + 16), i); return "SHA-224" === e ? [i[0], i[1], i[2], i[3], i[4], i[5], i[6]] : i }(t, n, i, e, r) }, e.R = T(r), e.m = 512, e.F = "SHA-224" === r ? 224 : 256, e.g = !1, u.hmacKey && e.B(function (r, t, n, i) { var e = r + " must include a value and format"; if (!t) { if (!i) throw new Error(e); return i } if (void 0 === t.value || !t.format) throw new Error(e); return o(t.format, t.encoding || "UTF8", n)(t.value) }("hmacKey", u.hmacKey, e.u)), e } return function (t, n) { if ("function" != typeof n && null !== n) throw new TypeError("Class extends value " + String(n) + " is not a constructor or null"); function i() { this.constructor = t } r(t, n), t.prototype = null === n ? Object.create(n) : (i.prototype = n.prototype, new i) }(n, t), n }(function () { function r(r, t, n) { var i = n || {}; if (this.v = t, this.l = i.encoding || "UTF8", this.numRounds = i.numRounds || 1, isNaN(this.numRounds) || this.numRounds !== parseInt(this.numRounds, 10) || 1 > this.numRounds) throw new Error("numRounds must a integer >= 1"); this.S = r, this.H = [], this.Y = 0, this.C = !1, this.I = 0, this.L = !1, this.N = [], this.X = [] } return r.prototype.update = function (r) { var t, n = 0, i = this.m >>> 5, e = this.h(r, this.H, this.Y), o = e.binLen, u = e.value, s = o >>> 5; for (t = 0; t < s; t += i)n + this.m <= o && (this.R = this.p(u.slice(t, t + i), this.R), n += this.m); return this.I += n, this.H = u.slice(n >>> 5), this.Y = o % this.m, this.C = !0, this }, r.prototype.getHash = function (r, t) { var n, i, e = this.F, o = a(t); if (this.g) { if (-1 === o.outputLen) throw new Error("Output length must be specified in options"); e = o.outputLen } var s = u(r, e, this.u, o); if (this.L && this.t) return s(this.t(o)); for (i = this.T(this.H.slice(), this.Y, this.I, this.A(this.R), e), n = 1; n < this.numRounds; n += 1)this.g && e % 32 != 0 && (i[i.length - 1] &= 16777215 >>> 24 - e % 32), i = this.T(i, e, 0, this.U(this.S), e); return s(i) }, r.prototype.setHMACKey = function (r, t, n) { if (!this.o) throw new Error("Variant does not support HMAC"); if (this.C) throw new Error("Cannot set MAC key after calling update"); var i = o(t, (n || {}).encoding || "UTF8", this.u); this.B(i(r)) }, r.prototype.B = function (r) { var t, n = this.m >>> 3, i = n / 4 - 1; if (1 !== this.numRounds) throw new Error("Cannot set numRounds with MAC"); if (this.L) throw new Error("MAC key already set"); for (n < r.binLen / 8 && (r.value = this.T(r.value, r.binLen, 0, this.U(this.S), this.F)); r.value.length <= i;)r.value.push(0); for (t = 0; t <= i; t += 1)this.N[t] = 909522486 ^ r.value[t], this.X[t] = 1549556828 ^ r.value[t]; this.R = this.p(this.N, this.R), this.I = this.m, this.L = !0 }, r.prototype.getHMAC = function (r, t) { var n = a(t); return u(r, this.F, this.u, n)(this.i()) }, r.prototype.i = function () { var r; if (!this.L) throw new Error("Cannot call getHMAC without first setting MAC key"); var t = this.T(this.H.slice(), this.Y, this.I, this.A(this.R), this.F); return r = this.p(this.X, this.U(this.S)), r = this.T(t, this.F, this.m, r, this.F) }, r }()) })); const sado = `@import url('https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&f[]=general-sans@400,500&display=swap');:root{--bg-color:#111827;--text-color:#F9FAFB;--primary-accent-color:#634999;--secondary-text-color:#94A3B8;--shadow-color-dark:rgba(0,0,0,0.25);--shadow-color-light:rgba(255,255,255,0.03);--style-accent-teal:#14B8A6;--style-accent-green:#22C55E;--style-accent-orange:#F97316;--style-accent-gray:#6B7280;--success-color:#22C55E;--error-color:#EF4444;--info-color:#3B82F6;--warning-color:#F59E0B;--border-color:rgba(148,163,184,0.1);--border-color-hover:rgba(148,163,184,0.2);--border-color-focus:var(--primary-accent-color);--input-bg-color:rgba(255,255,255,0.04);--input-bg-color-hover:rgba(255,255,255,0.06);--input-bg-color-focus:rgba(255,255,255,0.07);--button-secondary-bg:rgba(255,255,255,0.05);--button-secondary-bg-hover:rgba(255,255,255,0.08);--button-secondary-bg-active:rgba(0,0,0,0.05);--border-radius:8px;--shadow-base:3px 3px 8px var(--shadow-color-dark),-2px -2px 4px var(--shadow-color-light);--shadow-inset:inset 2px 2px 4px var(--shadow-color-dark),inset -2px -2px 4px var(--shadow-color-light);--shadow-concave:4px 4px 8px var(--shadow-color-dark),-4px -4px 8px var(--shadow-color-light),inset 1px 1px 2px var(--shadow-color-light),inset -1px -1px 2px var(--shadow-color-dark);}#mz_tactics_panel{font-family:"Satoshi",-apple-system,sans-serif;background-color:var(--bg-color);border-radius:12px;padding:16px 16px 4px 16px;margin:8px;box-shadow:var(--shadow-base);border:1px solid var(--border-color);transition:max-height 0.4s ease-out,padding 0.4s ease-out,margin 0.4s ease-out,opacity 0.3s ease-out;max-height:1000px;opacity:1;color:var(--text-color);overflow:visible;}#mz_tactics_panel.collapsed{max-height:0 !important;padding-top:0 !important;padding-bottom:0 !important;margin-top:0 !important;margin-bottom:0 !important;opacity:0 !important;border:none !important;overflow:hidden !important;}.mz-group{background-color:rgba(0,0,0,0.1);border-radius:var(--border-radius);padding:12px;margin:8px 0;box-shadow:none;border:1px solid var(--border-color);position:relative;transition:max-height 0.3s ease-out,padding 0.3s ease-out,margin 0.3s ease-out;overflow:visible;}.mz-group-main-title{display:flex;justify-content:space-between;align-items:center;color:var(--text-color);font-size:18px;font-weight:500;margin:-4px 0 12px 0;padding-bottom:8px;border-bottom:1px solid var(--border-color);}.mz-title-container{display:flex;align-items:center;gap:8px;flex-grow:1;flex-wrap:nowrap;min-width:0;}.mz-main-title{font-family:"Satoshi",sans-serif;font-size:20px;font-weight:700;margin:0;padding:0;letter-spacing:0.2px;white-space:nowrap;background:linear-gradient(to right,var(--primary-accent-color),#A78BFA);-webkit-background-clip:text;background-clip:text;color:transparent;}.mz-version-text{color:#2A8C5E;opacity:0.7;font-size:0.9em;font-weight:400;margin-right:10px;white-space:nowrap;}.mz-divider{width:50px;height:1px;background:var(--border-color);margin:10px auto 0;opacity:0.5;}#toggle_panel_btn{background:transparent;border:none;color:var(--secondary-text-color);cursor:pointer;padding:8px;width:32px;height:32px;border-radius:50%;margin-left:auto;font-size:18px;transition:all 0.3s ease;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;}#toggle_panel_btn:hover{background:rgba(255,255,255,0.05);color:var(--text-color);}#collapsed_icon{display:none;width:20px;height:18px;border-radius:50%;background:#1F2937;color:var(--secondary-text-color);font-size:9px;font-weight:bold;text-align:center;line-height:18px;cursor:pointer;transition:transform 0.3s ease,box-shadow 0.3s ease,opacity 0.3s ease;box-shadow:var(--shadow-base);border:1px solid var(--border-color);margin-left:8px;vertical-align:middle;flex-shrink:0;opacity:0;transform:scale(0.8);}#collapsed_icon.visible{display:inline-block;opacity:1;transform:scale(1);}#collapsed_icon:hover{transform:scale(1.1);box-shadow:0 0 10px rgba(139,92,246,0.3);color:var(--text-color);}#mz_tactics_panel .mzbtn{display:inline-flex;align-items:center;justify-content:center;padding:8px 14px;margin:4px;font-family:"Satoshi",sans-serif;font-size:13px;font-weight:500;color:var(--text-color);background:var(--button-secondary-bg);border:1px solid var(--border-color);border-radius:var(--border-radius);cursor:pointer;transition:all 0.2s ease;min-height:36px;box-shadow:0 1px 2px rgba(0,0,0,0.1);}#mz_tactics_panel .mzbtn:hover:not(:disabled){background:var(--button-secondary-bg-hover);border-color:var(--border-color-hover);transform:translateY(-1px);box-shadow:0 3px 6px rgba(0,0,0,0.15);}#mz_tactics_panel .mzbtn:active:not(:disabled){background:var(--button-secondary-bg-active);transform:translateY(0);box-shadow:inset 0 1px 2px rgba(0,0,0,0.2);}#mz_tactics_panel .mzbtn:disabled{opacity:0.5;cursor:not-allowed;}#mz_tactics_panel select{font-family:"Satoshi",sans-serif;font-size:14px;color:var(--text-color);padding:7px 13px;border:1px solid var(--border-color);border-radius:var(--border-radius);background-color:var(--input-bg-color);cursor:pointer;margin:0;transition:all 0.2s ease;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url("data:image/svg+xml;utf8,<svg fill='%2394A3B8' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/></svg>");background-repeat:no-repeat;background-position:right 10px top 50%;padding-right:30px;height:36px;box-sizing:border-box;}#mz_tactics_panel select:hover:not(:disabled){background:var(--input-bg-color-hover);border-color:var(--border-color-hover);}#mz_tactics_panel select:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);background:var(--input-bg-color-focus);}#mz_tactics_panel select option{background-color:#1F2937;color:var(--text-color);padding:5px 10px;}#mz_tactics_panel select:disabled{opacity:0.5;cursor:not-allowed;}.tactics-selector-section{margin-bottom:12px;}.tactics-selector-label{display:block;margin-bottom:5px;color:var(--secondary-text-color);font-size:12px;}.modal-info-wrapper{width:100%;max-width:550px;}.modal-tabs{display:flex;border-bottom:1px solid var(--border-color);margin-bottom:15px;}.modal-tab{padding:10px 15px;cursor:pointer;border:none;background:none;color:var(--secondary-text-color);opacity:0.8;border-bottom:2px solid transparent;transition:all 0.3s ease;font-family:"Satoshi",sans-serif;font-size:14px;font-weight:500;}.modal-tab:hover{opacity:1;color:var(--text-color);background-color:rgba(255,255,255,0.03);}.modal-tab.active{opacity:1;color:var(--primary-accent-color);border-bottom-color:var(--primary-accent-color);font-weight:600;}.modal-tab-content{display:none;padding:5px;max-height:40vh;overflow-y:auto;overflow-x:hidden;scrollbar-width:thin;scrollbar-color:var(--primary-accent-color) rgba(255,255,255,0.05);}#mz_tactics_panel .mztm-custom-select-list-container{background-color:#1F2937;}#mz_tactics_panel #category-selector option{background-color:#1F2937;}.modal-tab-content::-webkit-scrollbar{width:6px;}.modal-tab-content::-webkit-scrollbar-track{background:rgba(255,255,255,0.05);border-radius:3px;}.modal-tab-content::-webkit-scrollbar-thumb{background-color:var(--primary-accent-color);border-radius:3px;}.modal-tab-content.active{display:block;animation:fadeIn 0.5s ease;}.modal-tab-content[data-tab-id="links"] a{color:var(--info-color);}.faq-section h3{color:var(--primary-accent-color);margin-top:20px;margin-bottom:15px;font-size:16px;border-bottom:1px solid var(--border-color);padding-bottom:5px;}.faq-item{margin-bottom:15px;}.faq-item h4{font-weight:600;margin-bottom:5px;color:var(--text-color);font-size:14px;}.faq-item p{font-size:13px;line-height:1.5;color:var(--secondary-text-color);margin:0;}.faq-item code{background-color:var(--input-bg-color);padding:2px 5px;border-radius:4px;font-size:0.9em;color:var(--primary-accent-color);border:1px solid var(--border-color);}.info-modal-content a{color:var(--primary-accent-color);text-decoration:none;transition:color 0.3s ease;}.info-modal-content a:hover{color:var(--primary-accent-color);opacity:0.8;text-decoration:underline;}.info-modal-content ul{list-style:none;padding:0;}.info-modal-content ul li{margin:12px 0;padding:8px 12px;border-radius:var(--border-radius);background:var(--input-bg-color);border:1px solid var(--border-color);transition:all 0.3s ease;}.info-modal-content ul li:hover{background:var(--input-bg-color-hover);}#mz-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background-color:rgba(17,24,39,0.8);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;transition:opacity 0.3s ease;}#mz-modal-container{background:var(--bg-color);border-radius:12px;padding:24px;box-shadow:0 10px 25px rgba(0,0,0,0.3);border:1px solid var(--border-color);max-width:550px;width:90%;transform:scale(0.9);transition:transform 0.3s ease;color:var(--text-color);font-family:"Satoshi",-apple-system,sans-serif;}#mz-modal-container.management-modal{max-width:700px;width:95%;}#mz-modal-overlay.active{opacity:1;}#mz-modal-overlay.active #mz-modal-container{transform:scale(1);}#mz-modal-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;border-bottom:none;padding-bottom:0;}#mz-modal-title{font-size:20px;font-weight:500;margin:0;}#mz-modal-close{background:transparent;border:none;color:var(--secondary-text-color);font-size:22px;cursor:pointer;transition:all 0.3s ease;padding:0;width:36px;height:36px;display:flex;align-items:center;justify-content:center;border-radius:50%;}#mz-modal-close:hover{background:rgba(255,255,255,0.05);color:var(--error-color);}#mz-modal-content{margin-bottom:24px;white-space:pre-line;line-height:1.5;color:var(--secondary-text-color);}#mz-modal-input{width:calc(100% - 32px);background:var(--input-bg-color);border:1px solid var(--border-color);color:var(--text-color);padding:14px 16px;border-radius:var(--border-radius);font-family:"Satoshi",sans-serif;font-size:15px;margin-bottom:20px;transition:all 0.3s ease;box-sizing:border-box;}#mz-modal-input:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);background:var(--input-bg-color-focus);}.mz-modal-label{display:block;margin-bottom:8px;font-size:14px;color:var(--text-color);opacity:0.9;margin-top:10px;}#mz-modal-description{width:calc(100% - 0);background:var(--input-bg-color);border:1px solid var(--border-color);color:var(--text-color);padding:12px 16px;border-radius:var(--border-radius);font-family:"Satoshi",sans-serif;font-size:14px;margin-bottom:20px;transition:all 0.3s ease;box-sizing:border-box;resize:vertical;min-height:60px;}#mz-modal-description:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);background:var(--input-bg-color-focus);}#mz-modal-buttons{display:flex;justify-content:flex-start;gap:12px;margin-top:15px;}.mz-modal-btn{display:inline-flex;align-items:center;justify-content:center;padding:10px 18px;font-family:"Satoshi",sans-serif;font-size:15px;font-weight:500;color:var(--text-color);background:var(--button-secondary-bg);border:1px solid var(--border-color);border-radius:var(--border-radius);cursor:pointer;transition:all 0.2s ease;min-width:90px;box-shadow:0 1px 2px rgba(0,0,0,0.1);}.mz-modal-btn:hover{background:var(--button-secondary-bg-hover);border-color:var(--border-color-hover);transform:translateY(-1px);box-shadow:0 3px 6px rgba(0,0,0,0.15);}.mz-modal-btn:active{background:var(--button-secondary-bg-active);transform:translateY(0);box-shadow:inset 0 1px 2px rgba(0,0,0,0.2);}.mz-modal-btn.primary{background:var(--primary-accent-color);color:#ffffff;font-weight:500;border:none;}.mz-modal-btn.primary:hover{background-color:#7C3AED;}.mz-modal-btn.cancel{background:transparent;color:var(--secondary-text-color);border:1px solid var(--border-color);}.mz-modal-btn.cancel:hover{background:rgba(255,255,255,0.03);color:var(--text-color);border-color:var(--border-color-hover);}.mz-modal-icon{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:50%;margin-right:14px;font-weight:bold;font-size:1.2em;}.mz-modal-icon.success{color:var(--success-color);background:rgba(34,197,94,0.1);}.mz-modal-icon.error{color:var(--error-color);background:rgba(239,68,68,0.1);}.mz-modal-icon.info{color:var(--info-color);background:rgba(59,130,246,0.1);}.mz-modal-title-with-icon{display:flex;align-items:center;}.formations-controls-container{display:flex;flex-wrap:nowrap;gap:8px;margin-top:0;align-items:center;}.mztm-filter-controls-row{display:flex;flex-wrap:nowrap;gap:8px;align-items:center;width:100%;}.tactics-search-box{width:140px !important;padding:8px 12px;margin-bottom:0 !important;border:1px solid var(--border-color);border-radius:var(--border-radius);background-color:var(--input-bg-color);color:var(--text-color);font-family:"Satoshi",sans-serif;font-size:14px;box-sizing:border-box;height:36px;transition:all 0.2s ease;position:relative;flex-shrink:0;}.tactics-search-box:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);background:var(--input-bg-color-focus);}.tactics-search-box.filtering{border-bottom:2px solid var(--primary-accent-color);animation:pulse-border 1.5s infinite;}@keyframes pulse-border{0%{border-color:var(--primary-accent-color);}50%{border-color:transparent;}100%{border-color:var(--primary-accent-color);}}.category-filter-wrapper{display:flex;align-items:center;gap:4px;flex-shrink:0;}#category_filter_selector{min-width:140px;flex-grow:0;}#manage_items_btn{padding:4px 8px;min-width:auto;height:36px;font-size:14px;line-height:1;flex-shrink:0;color:var(--secondary-text-color);}#manage_items_btn:hover{color:var(--text-color);}.mztm-custom-select-wrapper{position:relative;flex:1;min-width:180px;flex-grow:1;flex-shrink:1;}.mztm-custom-select-trigger{display:flex;align-items:center;justify-content:space-between;font-family:"Satoshi",sans-serif;font-size:14px;color:var(--text-color);padding:8px 14px;border:1px solid var(--border-color);border-radius:var(--border-radius);background-color:var(--input-bg-color);cursor:pointer;margin:0;transition:all 0.2s ease;height:36px;box-sizing:border-box;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}.mztm-custom-select-trigger:hover:not(.disabled){background:var(--input-bg-color-hover);border-color:var(--border-color-hover);}.mztm-custom-select-trigger.open{border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);border-bottom-left-radius:0;border-bottom-right-radius:0;background:var(--input-bg-color-focus);}.mztm-custom-select-trigger.disabled{opacity:0.5;cursor:not-allowed;}.mztm-custom-select-trigger::after{content:'';border:solid var(--secondary-text-color);border-width:0 2px 2px 0;display:inline-block;padding:3px;transform:rotate(45deg);-webkit-transform:rotate(45deg);margin-left:10px;flex-shrink:0;transition:border-color 0.2s ease;}.mztm-custom-select-trigger:hover:not(.disabled)::after{border-color:var(--text-color);}.mztm-custom-select-trigger.open::after{transform:rotate(-135deg);-webkit-transform:rotate(-135deg);margin-top:3px;}.mztm-custom-select-placeholder{color:var(--secondary-text-color);font-style:italic;}.mztm-custom-select-list-container{position:absolute;top:100%;left:0;right:0;background-color:#1F2937;border:1px solid var(--border-color-focus);border-top:none;border-radius:0 0 8px 8px;z-index:1001;max-height:250px;overflow-y:auto;box-shadow:0 5px 10px rgba(0,0,0,0.2);display:none;scrollbar-width:thin;scrollbar-color:var(--primary-accent-color) rgba(255,255,255,0.05);}.mztm-custom-select-list-container.open{display:block;animation:fadeIn 0.2s ease-out;}.mztm-custom-select-list-container::-webkit-scrollbar{width:6px;}.mztm-custom-select-list-container::-webkit-scrollbar-track{background:rgba(255,255,255,0.05);border-radius:3px;}.mztm-custom-select-list-container::-webkit-scrollbar-thumb{background-color:var(--primary-accent-color);border-radius:3px;}.mztm-custom-select-list{list-style:none;padding:0;margin:0;}.mztm-custom-select-category{color:var(--primary-accent-color);font-size:11px;font-weight:600;padding:6px 12px;background:rgba(0,0,0,0.2);margin-top:2px;border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color);cursor:default;text-transform:uppercase;letter-spacing:0.5px;}.mztm-custom-select-item{padding:8px 12px;cursor:pointer;transition:background-color 0.15s ease,color 0.15s ease;font-size:14px;color:var(--text-color);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.mztm-custom-select-item:hover,.mztm-custom-select-item.highlighted{background-color:var(--primary-accent-color);color:#ffffff;}.mztm-custom-select-item.disabled{opacity:0.5;cursor:default;background-color:transparent !important;color:var(--secondary-text-color) !important;}.mztm-custom-select-item.hidden{display:none;}.mztm-custom-select-no-results{padding:10px 12px;color:var(--secondary-text-color);font-style:italic;cursor:default;}.tactics-style-indicator{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;}.tactics-style-indicator.accent-teal{background-color:var(--style-accent-teal);}.tactics-style-indicator.accent-green{background-color:var(--style-accent-green);}.tactics-style-indicator.accent-orange{background-color:var(--style-accent-orange);}.tactics-style-indicator.accent-gray{background-color:var(--style-accent-gray);}#category-selector{width:75%;padding:10px 12px;border:1px solid var(--border-color);border-radius:var(--border-radius);background-color:var(--input-bg-color);color:var(--text-color);font-family:"Satoshi",sans-serif;font-size:13px;box-sizing:border-box;}#category-selector option{padding:8px;background-color:#1F2937;}.category-selection-container{margin-bottom:10px;}.category-selection-label{display:block;margin-bottom:5px;font-size:14px;color:var(--text-color);opacity:0.9;}.new-category-input-container{margin-top:10px;display:none;}.new-category-input-container.visible{display:block;}#new-category-input{width:100%;padding:10px 12px;border:1px solid var(--border-color);border-radius:var(--border-radius);background-color:var(--input-bg-color);color:var(--text-color);font-family:"Satoshi",sans-serif;font-size:14px;box-sizing:border-box;}#new-category-input:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);background:var(--input-bg-color-focus);}@keyframes fadeIn{from{opacity:0;transform:translateY(-5px);}to{opacity:1;transform:translateY(0);}}@keyframes shake{0%,100%{transform:translateX(0);}25%{transform:translateX(-2px);}50%{transform:translateX(0);}75%{transform:translateX(2px);}}.mztm-custom-select-wrapper.filtering .mztm-custom-select-trigger{border-color:var(--primary-accent-color);animation:pulse-border 1.5s infinite;}.action-buttons-section{display:flex;flex-direction:column;flex-wrap:nowrap;margin-top:10px;gap:4px;}.action-buttons-row{display:flex;flex-wrap:wrap;gap:6px;width:100%;}.footer-actions{position:absolute;bottom:10px;right:16px;background:transparent !important;border:none !important;box-shadow:none !important;font-family:"General Sans",sans-serif !important;color:gold !important;opacity:0.8;transition:opacity 0.2s ease;}.footer-actions:hover{opacity:1;transform:none !important;}#manage_action_dropdown_menu{max-height:200px;overflow-y:auto;overflow-x:hidden;}#loading-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(17,24,39,0.7);backdrop-filter:blur(3px);display:flex;justify-content:center;align-items:center;z-index:10001;opacity:0;transition:opacity 0.3s ease;pointer-events:none;}#loading-overlay.visible{opacity:1;pointer-events:auto;}#loading-spinner{border:4px solid rgba(255,255,255,0.1);border-left-color:var(--primary-accent-color);border-radius:50%;width:40px;height:40px;animation:spin 1s linear infinite;}@keyframes spin{to{transform:rotate(360deg);}}.mode-toggle-container{display:flex;align-items:center;margin:0 10px 0 30px;white-space:nowrap;}.mode-toggle-switch{position:relative;display:inline-block;width:44px;height:22px;margin:0 10px;vertical-align:middle;}.mode-toggle-switch input{opacity:0;width:0;height:0;}.mode-toggle-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#4B5563;transition:0.4s;border-radius:22px;border:1px solid var(--border-color);}.mode-toggle-slider:before{position:absolute;content:"";height:16px;width:16px;left:3px;bottom:2px;background-color:white;transition:0.4s;border-radius:50%;}.mode-toggle-switch input:checked + .mode-toggle-slider{background-color:var(--primary-accent-color);}.mode-toggle-switch input:checked + .mode-toggle-slider:before{transform:translateX(22px);}.mode-toggle-label{font-size:12px;vertical-align:middle;color:var(--secondary-text-color);opacity:0.8;transition:opacity 0.2s ease,color 0.2s ease;}.mode-toggle-label.active{opacity:1;color:var(--text-color);font-weight:500;}.section-content{position:relative;padding-bottom:2px;overflow:visible;}.management-modal-wrapper{width:100%;}.management-modal-content{display:none;padding:5px;max-height:50vh;overflow-y:auto;overflow-x:hidden;scrollbar-width:thin;scrollbar-color:var(--primary-accent-color) rgba(255,255,255,0.05);}.management-modal-content::-webkit-scrollbar{width:8px;}.management-modal-content::-webkit-scrollbar-track{background:rgba(255,255,255,0.05);border-radius:4px;}.management-modal-content::-webkit-scrollbar-thumb{background-color:var(--primary-accent-color);border-radius:4px;}.management-modal-content.active{display:block;animation:fadeIn 0.5s ease;}.formation-management-list,.category-management-list{list-style:none;padding:0;margin:10px 0 0 0;}.formation-management-list li,.category-management-list li{display:flex;justify-content:space-between;align-items:center;padding:10px 5px;border-bottom:1px solid var(--border-color);transition:background-color 0.2s ease;}.formation-management-list li:last-child,.category-management-list li:last-child{border-bottom:none;}.item-name-container{flex-grow:1;margin-right:10px;display:flex;align-items:center;min-width:0;}.item-name{flex-grow:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-color);}.item-name-input{flex-grow:1;background:var(--input-bg-color-hover);border:1px solid var(--border-color-hover);color:var(--text-color);padding:4px 8px;border-radius:4px;font-family:"Satoshi",sans-serif;font-size:14px;margin-right:5px;}.item-name-input:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);}.item-controls{display:flex;align-items:center;gap:8px;flex-shrink:0;}.item-category-select{padding:4px 8px !important;height:30px !important;font-size:12px !important;min-width:120px;background-position:right 5px top 50%;padding-right:25px !important;flex-shrink:0;background-color:var(--input-bg-color);border-color:var(--border-color);color:var(--secondary-text-color);}.item-category-select:hover{background-color:var(--input-bg-color-hover);border-color:var(--border-color-hover);color:var(--text-color);}.item-action-btn{background:transparent;border:none;color:var(--secondary-text-color);opacity:0.7;cursor:pointer;font-size:16px;padding:4px;transition:opacity 0.2s ease,color 0.2s ease;line-height:1;}.item-action-btn:hover{opacity:1;color:var(--text-color);}.save-name-btn{color:var(--success-color);}.cancel-name-btn{color:var(--error-color);}.delete-item-btn{color:var(--error-color);}.delete-item-btn:hover{color:var(--error-color);}.add-category-section{display:flex;gap:8px;padding:10px 5px;border-bottom:1px solid var(--border-color);margin-bottom:10px;}.add-category-input{flex-grow:1;background:var(--input-bg-color);border:1px solid var(--border-color);color:var(--text-color);padding:8px 12px;border-radius:var(--border-radius);font-family:"Satoshi",sans-serif;font-size:14px;}.add-category-input:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);background:var(--input-bg-color-focus);}.add-category-btn{padding:8px 15px !important;font-size:14px !important;min-width:auto !important;background:var(--primary-accent-color) !important;color:white !important;border:none !important;}.add-category-btn:hover{background:#7C3AED !important;transform:none !important;box-shadow:none !important;}.category-management-list li span{flex-grow:1;margin-right:10px;color:var(--text-color);}.category-remove-btn{background-color:rgba(239,68,68,0.1) !important;border:1px solid rgba(239,68,68,0.3) !important;color:var(--error-color) !important;padding:5px 10px !important;min-width:auto !important;font-size:13px !important;}.category-remove-btn:hover{background-color:rgba(239,68,68,0.2) !important;border-color:rgba(239,68,68,0.5) !important;color:#FECACA !important;transform:none !important;box-shadow:none !important;}.no-items-message,.no-custom-categories-message{padding:15px;text-align:center;color:var(--secondary-text-color);}#mztm-tactic-preview{position:fixed;z-index:10002;background-color:#1F2937;color:var(--text-color);border:1px solid var(--border-color-hover);border-radius:var(--border-radius);padding:10px 15px;font-family:"Satoshi",sans-serif;font-size:13px;box-shadow:0 4px 12px rgba(0,0,0,0.2);max-width:250px;pointer-events:none;line-height:1.5;opacity:0;transition:opacity 0.2s ease-in-out;white-space:normal;display:none;}.mztm-preview-formation{margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--border-color);font-size:14px;}.mztm-preview-formation strong{color:var(--primary-accent-color);font-weight:500;}.mztm-preview-desc{max-height:150px;overflow-y:auto;word-wrap:break-word;scrollbar-width:thin;color:var(--secondary-text-color);scrollbar-color:var(--primary-accent-color) rgba(255,255,255,0.05);}.mztm-preview-desc::-webkit-scrollbar{width:4px;}.mztm-preview-desc::-webkit-scrollbar-track{background:rgba(255,255,255,0.05);border-radius:2px;}.mztm-preview-desc::-webkit-scrollbar-thumb{background-color:var(--primary-accent-color);border-radius:2px;}.mztm-preview-no-desc{color:var(--secondary-text-color);opacity:0.7;font-style:italic;}@media (max-width: 768px){#mz_tactics_panel{width:auto;margin:8px 4px;padding:12px;}.mz-group-main-title, .mz-title-container, .action-buttons-row{flex-wrap:wrap;}.formations-controls-container{flex-direction:column;flex-wrap:wrap;gap:10px;}.mztm-filter-controls-row{width:100%;flex-direction:column;gap:10px;}.mz-title-container{width:100%;margin-bottom:8px;justify-content:center;}.mode-toggle-container{width:100%;margin:8px 0 0 0;justify-content:center;}.mztm-custom-select-wrapper{width:100%;min-width:0;}.tactics-search-box{width:100% !important;}.category-filter-wrapper{width:100%;flex-direction:column;gap:10px;}#category_filter_selector{width:100%;}#manage_items_btn{width:100%;}.action-buttons-section{margin-top:12px;}.action-buttons-row{flex-direction:column;gap:8px;width:100%;}#mz_tactics_panel .mzbtn{width:100%;margin:0;}#mz-modal-container{width:95vw;max-width:95vw;padding:16px;}#mz-modal-container.management-modal{width:98vw;max-width:98vw;}#mz-modal-buttons{flex-direction:column-reverse;gap:8px;}.mz-modal-btn{width:100%;margin:0;}.formation-management-list li, .category-management-list li{flex-direction:column;align-items:flex-start;gap:10px;}.item-name-container{width:100%;}.item-controls{width:100%;justify-content:space-between;}.item-category-select{flex-grow:1;}.footer-actions{position:relative;bottom:auto;right:auto;width:100%;margin-top:10px;text-align:center;}}}` GM_addStyle(sado); const OUTFIELD_PLAYERS_SELECTOR = '.fieldpos.fieldpos-ok.ui-draggable:not(.substitute):not(.goalkeeper):not(.substitute.goalkeeper), .fieldpos.fieldpos-collision.ui-draggable:not(.substitute):not(.goalkeeper):not(.substitute.goalkeeper)'; const GOALKEEPER_SELECTOR = '.fieldpos.fieldpos-ok.goalkeeper.ui-draggable'; const FORMATION_TEXT_SELECTOR = '#formation_text'; const TACTIC_SLOT_SELECTOR = '.ui-state-default.ui-corner-top.ui-tabs-selected.ui-state-active.invalid'; const MIN_PLAYERS_ON_PITCH = 11; const MAX_TACTIC_NAME_LENGTH = 50; const MAX_CATEGORY_NAME_LENGTH = 30; const MAX_DESCRIPTION_LENGTH = 250; const SCRIPT_VERSION = '15'; const DISPLAY_VERSION = '15'; const SCRIPT_NAME = 'MZ Tactics Manager'; const VERSION_KEY = 'mz_tactics_version'; const COLLAPSED_KEY = 'mz_tactics_collapsed'; const VIEW_MODE_KEY = 'mztm_view_mode'; const CATEGORIES_STORAGE_KEY = 'mz_tactics_categories'; const FORMATIONS_STORAGE_KEY = 'mztm_formations'; const OLD_FORMATIONS_STORAGE_KEY = 'ls_tactics'; const COMPLETE_TACTICS_STORAGE_KEY = 'mztm_complete_tactics'; const ROSTER_CACHE_KEY = 'mztm_roster_cache'; const USER_INFO_CACHE_KEY = 'mztm_user_info_cache'; const CATEGORY_FILTER_STORAGE_KEY = 'mztm_last_category_filter'; const ROSTER_CACHE_DURATION_MS = 3600000; const USER_INFO_CACHE_DURATION_MS = 86400000; const INITIAL_FORMATIONS_DATA = {"tactics": [{"name": "SP_3d_1dm (1)","coordinates": [[96,54],[147,54],[120,94],[96,141],[139,169],[52,171],[96,188],[139,216],[54,217],[97,236]]},{"name": "SP_3d_1dm (2)","coordinates": [[101,54],[74,96],[120,97],[96,137],[55,163],[137,163],[96,184],[55,210],[137,210],[96,231]]},{"name": "SP_3d_1dm (3)","coordinates": [[97,54],[138,78],[97,101],[96,147],[56,170],[136,171],[96,194],[56,218],[136,218],[96,241]]},{"name": "SP_3d_1dm (4)","coordinates": [[118,54],[97,95],[144,95],[97,148],[57,172],[137,172],[97,195],[57,219],[137,219],[97,242]]},{"name": "SP_3d_1dm (5)","coordinates": [[125,54],[82,77],[122,101],[81,126],[56,167],[136,167],[96,190],[56,214],[136,214],[96,237]]},{"name": "SP_3d_mand (1)","coordinates": [[96,54],[136,77],[96,101],[136,124],[96,148],[72,188],[120,188],[49,228],[96,228],[143,228]]},{"name": "SP_3d_morph (1)","coordinates": [[86,54],[154,54],[120,85],[96,125],[143,125],[72,165],[119,165],[49,205],[96,205],[143,205]]},{"name": "SP_3d_morph (2)","coordinates": [[82,55],[161,56],[124,88],[157,121],[96,125],[72,165],[119,165],[49,205],[96,205],[143,205]]},{"name": "SP_4d_2dm_c (1)","coordinates": [[100,54],[73,96],[123,98],[97,136],[73,176],[120,176],[26,209],[167,209],[73,224],[120,224]]},{"name": "SP_4d_2dm_c (2)","coordinates": [[69,54],[129,54],[97,92],[97,139],[73,179],[120,179],[26,209],[167,209],[73,227],[120,227]]},{"name": "SP_4d_2dm_c (3)","coordinates": [[97,54],[145,54],[119,98],[97,146],[73,186],[120,186],[26,209],[167,209],[73,234],[120,234]]},{"name": "SP_4d_2dm_w (1)","coordinates": [[77,54],[144,54],[111,87],[96,134],[70,172],[122,172],[3,217],[191,217],[73,219],[120,220]]},{"name": "SP_4d_2dm_w (2)","coordinates": [[91,55],[130,83],[85,103],[98,149],[121,190],[75,191],[5,214],[189,214],[74,237],[121,237]]},{"name": "SP_4d_3dm (1)","coordinates": [[100,54],[74,99],[121,99],[50,174],[144,174],[96,176],[26,216],[73,216],[120,216],[167,216]]},{"name": "SP_4d_3dm (2)","coordinates": [[92,54],[143,54],[113,103],[97,160],[45,174],[151,176],[3,215],[187,215],[74,218],[128,218]]},{"name": "SP_5d_2dm (1)","coordinates": [[106,54],[81,93],[96,137],[73,177],[120,177],[2,205],[190,205],[49,217],[96,217],[143,217]]},{"name": "SP_5d_2dm (2)","coordinates": [[101,72],[75,118],[121,122],[73,177],[120,177],[2,205],[190,205],[49,217],[96,217],[143,217]]},{"name": "SP_5d_2dm (3)","coordinates": [[66,54],[126,54],[96,125],[73,165],[120,165],[2,205],[49,205],[96,205],[143,205],[190,205]]},{"name": "WP_2d_3dm_1w (1)","coordinates": [[125,56],[190,60],[78,71],[97,190],[49,191],[146,191],[191,215],[3,218],[74,230],[121,230]]},{"name": "WP_3d_1dm_1w (1)","coordinates": [[124,54],[191,60],[82,73],[97,145],[57,168],[137,170],[97,194],[57,217],[137,217],[97,241]]},{"name": "WP_3d_2dm_1w (1)","coordinates": [[128,54],[189,62],[78,69],[120,148],[72,149],[73,195],[120,195],[50,235],[97,235],[143,236]]},{"name": "WP_3d_3dm_1w (1)","coordinates": [[188,67],[79,72],[124,102],[98,142],[74,182],[121,182],[168,182],[51,222],[98,222],[145,222]]},{"name": "WP_4d_2dm_1w_c (1)","coordinates": [[125,54],[191,60],[82,71],[97,150],[74,192],[121,192],[34,216],[161,216],[74,240],[121,240]]},{"name": "WP_4d_2dm_1w_c (2)","coordinates": [[191,60],[82,71],[123,92],[97,150],[74,192],[121,192],[34,216],[161,216],[74,240],[121,240]]},{"name": "WP_4d_2dm_1w_w (1)","coordinates": [[122,54],[189,58],[79,72],[97,142],[73,182],[122,182],[7,209],[187,209],[72,228],[123,228]]},{"name": "WP_4d_2dm_2w_c (1)","coordinates": [[78,71],[191,72],[164,110],[96,153],[72,193],[120,193],[30,220],[162,220],[72,240],[120,240]]},{"name": "WP_4d_3dm_1w_w (1)","coordinates": [[126,54],[190,61],[80,72],[50,174],[97,174],[144,174],[30,217],[77,217],[124,217],[171,217]]},{"name": "WP_4d_2dm_1w_w (2)","coordinates": [[124,54],[189,54],[77,71],[97,128],[96,176],[144,176],[26,216],[73,216],[120,216],[167,216]]},{"name": "WP_5d_2dm_1w (1)","coordinates": [[190,57],[78,71],[96,123],[74,174],[121,174],[192,207],[4,208],[50,214],[97,214],[144,214]]},{"name": "WP_5d_2dm_2w (1)","coordinates": [[190,60],[81,71],[168,105],[122,177],[75,178],[5,207],[189,207],[50,217],[144,218],[97,219]]}]}; const DEFAULT_CATEGORIES = { 'short_passing': { id: 'short_passing', name: 'Short Passing', color: '#54a0ff' }, 'wing_play': { id: 'wing_play', name: 'Wing Play', color: '#5dd39e' } }; const NEW_CATEGORY_ID = 'new_category'; const OTHER_CATEGORY_ID = 'other'; const USERSCRIPT_STRINGS = { addButton: 'Add', addCurrentTactic: 'Add Current', addWithXmlButton: 'Add via XML', manageButton: 'Manage', deleteButton: 'Delete', renameButton: 'Edit', updateButton: 'Update Coords', clearButton: 'Clear', resetButton: 'Reset', importButton: 'Import', exportButton: 'Export', infoButton: 'FAQ', saveButton: 'Save', tacticNamePrompt: 'Please enter a name and a category', addAlert: 'Formation {} added successfully.', deleteAlert: 'Item {} deleted successfully.', renameAlert: 'Item {} successfully edited.', updateAlert: 'Formation {} updated successfully.', clearAlert: 'Formations cleared successfully.', resetAlert: 'Formations were reset successfully.', importAlert: 'Formations imported successfully.', exportAlert: 'Formations JSON copied to clipboard.', deleteConfirmation: 'Do you really want to delete {}?', updateConfirmation: 'Do you really want to update {} coords?', clearConfirmation: 'Do you really want to clear all saved formations?', resetConfirmation: 'Reset to default formations? This will remove all your custom formations.', invalidTacticError: 'Invalid formation. Ensure 11 players are on the pitch.', noTacticNameProvidedError: 'No name provided.', alreadyExistingTacticNameError: 'Name already exists.', tacticNameMaxLengthError: `Name is too long (max ${MAX_TACTIC_NAME_LENGTH} chars).`, noTacticSelectedError: 'No item selected.', duplicateTacticError: 'This formation already exists.', duplicateTacticErrorWithName: 'This formation already exists (name: {}).', noChangesMadeError: 'No changes detected in player positions.', invalidImportError: 'Invalid import data. Please provide valid JSON.', modalContentInfoText: 'MZ Tactics Manager by douglaskampl.', modalContentFeedbackText: 'For feedback or suggestions, contact via GB/Chat.', usefulContent: '', tacticsDropdownMenuLabel: 'Select a Formation', completeTacticsDropdownMenuLabel: 'Select a Tactic', errorTitle: 'Error', doneTitle: 'Success', confirmationTitle: 'Confirmation', deleteTacticConfirmButton: 'Delete', cancelConfirmButton: 'Cancel', updateConfirmButton: 'Update', clearTacticsConfirmButton: 'Clear', resetTacticsConfirmButton: 'Reset', addConfirmButton: 'Add', xmlValidationError: 'Invalid XML format', xmlParsingError: 'Error parsing XML', xmlPlaceholder: 'Paste Formation XML here', tacticNamePlaceholder: 'Formation name', managerTitle: SCRIPT_NAME, searchPlaceholder: 'Search...', allTacticsFilter: 'All', noTacticsFound: 'No formations found', welcomeMessage: `Userscript updated to v${SCRIPT_VERSION}.\n\nChanges in this version:\n• Userscript is now more mobile-friendly.`, welcomeGotIt: 'Got it!', removeCategoryConfirmation: 'Remove category "{}"? (All formations in this category will be moved to "Other").', removeCategoryAlert: 'Category "{}" removed successfully.', removeCategoryButton: 'Remove', completeTacticsTitle: 'Tactics Management', saveCompleteTacticButton: 'Save Current', loadCompleteTacticButton: 'Load', deleteCompleteTacticButton: 'Delete', renameCompleteTacticButton: 'Rename', updateCompleteTacticButton: 'Update with Current', importCompleteTacticsButton: 'Import', exportCompleteTacticsButton: 'Export', completeTacticNamePrompt: 'Please enter a name for the tactic', renameCompleteTacticPrompt: 'Enter a new name for the tactic:', updateCompleteTacticConfirmation: 'Overwrite tactic "{}" with the current setup (positions, rules, settings) from the pitch?', completeTacticSaveSuccess: 'Tactic {} saved successfully.', completeTacticLoadSuccess: 'Tactic {} loaded successfully.', completeTacticDeleteSuccess: 'Tactic {} deleted successfully.', completeTacticRenameSuccess: 'Tactic renamed to {} successfully.', completeTacticUpdateSuccess: 'Tactic {} updated successfully.', importCompleteTacticsTitle: 'Import Tactics (JSON)', exportCompleteTacticsTitle: 'Export Tactics (JSON)', importCompleteTacticsPlaceholder: 'Paste Tactics JSON here', importCompleteTacticsAlert: 'Tactics imported successfully.', exportCompleteTacticsAlert: 'Tactics JSON copied to clipboard.', invalidCompleteImportError: 'Invalid import data. Please provide valid JSON (object map).', errorFetchingRoster: 'Error fetching team roster. Cannot load Tactic.', errorInsufficientPlayers: 'Not enough available players in roster to fill required positions.', errorXmlExportParse: 'Error parsing XML from native export.', errorXmlGenerate: 'Error generating XML for import.', errorImportFailed: 'Native import failed. Check XML validity or player availability.', warningPlayersSubstituted: 'Warning: roster mismatch. Some were players replaced at random. Tactic updated!', invalidXmlForImport: 'MZ rejected the generated XML. It might be invalid or player assignments failed.', completeTacticNamePlaceholder: 'Tactic name', normalModeLabel: 'Formations', completeModeLabel: 'Tactics', modeLabel: '', manageCategoriesTitle: 'Manage Categories', noCustomCategories: 'No custom categories to manage.', manageCategoriesDoneButton: 'Done', managementModalTitle: 'Manage Formations & Categories', formationsTabTitle: 'Formations', categoriesTabTitle: 'Categories', addCategoryPlaceholder: 'New category name...', addCategoryButton: '+ Add', categoryNameMaxLengthError: `Category name too long (max ${MAX_CATEGORY_NAME_LENGTH} chars).`, saveChangesButton: 'Save Changes', changesSavedSuccess: 'Changes saved successfully.', noChangesToSave: 'No changes to save.', descriptionLabel: 'Description (optional):', descriptionPlaceholder: `Enter a short description (max ${MAX_DESCRIPTION_LENGTH} chars)...`, descriptionMaxLengthError: `Description too long (max ${MAX_DESCRIPTION_LENGTH} chars).`, previewFormationLabel: 'Formation:', xmlRequiredError: 'Please paste the XML data first.', invalidXmlFormatError: 'The provided text does not appear to be valid XML.', noTacticsSaved: 'No formations saved', noCompleteTacticsSaved: 'No tactics saved' }; const DEFAULT_MODAL_STRINGS = { ok: 'OK', cancel: 'Cancel', error: 'Error', close: '×' }; let tactics = []; let completeTactics = {}; let currentFilter = 'all'; let searchTerm = ''; let categories = {}; let rosterCache = { data: null, timestamp: 0, teamId: null }; let userInfoCache = { teamId: null, username: null, timestamp: 0 }; let teamId = null; let username = null; let loadingOverlay = null; let currentViewMode = 'normal'; let collapsedIconElement = null; let previewElement = null; let previewHideTimeout = null; let currentOpenDropdown = null; let selectedFormationTacticId = null; let selectedCompleteTacticName = null; function createModalIcon(type) { if (!type) return null; const i = document.createElement('div'); i.classList.add('mz-modal-icon'); if (type === 'success') { i.classList.add('success'); i.innerHTML = '✓'; } else if (type === 'error') { i.classList.add('error'); i.innerHTML = '✗'; } else if (type === 'info') { i.classList.add('info'); i.innerHTML = 'ℹ'; } return i; } function validateModalInput(inputElement, validatorFn, errorElementId) { if (!validatorFn || !inputElement || !inputElement.parentNode) return null; const validationError = validatorFn(inputElement.value); const existingError = document.getElementById(errorElementId); if (existingError) existingError.remove(); if (!validationError) return null; const errorContainer = document.createElement('div'); errorContainer.id = errorElementId; errorContainer.style.color = '#ff6b6b'; errorContainer.style.marginTop = inputElement.tagName === 'TEXTAREA' ? '5px' : '-10px'; errorContainer.style.marginBottom = '10px'; errorContainer.style.fontSize = '13px'; errorContainer.textContent = validationError; inputElement.parentNode.insertBefore(errorContainer, inputElement.nextSibling); return validationError; } function closeModal(overlayElement, callback) { if (!overlayElement) return; overlayElement.classList.remove('active'); setTimeout(() => { if (overlayElement && overlayElement.parentNode === document.body) document.body.removeChild(overlayElement); if (callback) callback(); }, 300); } function handleAlertConfirm(options, inputElement, descElement, categorySelect, newCategoryInput, overlayElement, resolve) { if (options.input === 'text' && options.inputValidator && inputElement) { const validationError = validateModalInput(inputElement, options.inputValidator, 'mz-modal-input-error'); if (validationError) return; } if (options.descriptionInput === 'textarea' && options.descriptionValidator && descElement) { const descValidationError = validateModalInput(descElement, options.descriptionValidator, 'mz-modal-desc-error'); if (descValidationError) return; } let selectedCategoryId = null; let newCategoryName = null; if (categorySelect) { selectedCategoryId = categorySelect.value; if (selectedCategoryId === NEW_CATEGORY_ID && newCategoryInput) { newCategoryName = newCategoryInput.value.trim(); const categoryErrorElement = document.getElementById('new-category-error'); if (categoryErrorElement) categoryErrorElement.remove(); if (!newCategoryName) { const errorText = document.createElement('div'); errorText.style.color = '#ff6b6b'; errorText.style.marginTop = '5px'; errorText.style.fontSize = '13px'; errorText.textContent = 'Category name cannot be empty.'; errorText.id = 'new-category-error'; newCategoryInput.parentNode.appendChild(errorText); return; } const existingCategory = Object.values(categories).find(cat => cat.name.toLowerCase() === newCategoryName.toLowerCase()); if (existingCategory) { const errorText = document.createElement('div'); errorText.style.color = '#ff6b6b'; errorText.style.marginTop = '5px'; errorText.style.fontSize = '13px'; errorText.textContent = 'Category name already exists.'; errorText.id = 'new-category-error'; newCategoryInput.parentNode.appendChild(errorText); return; } } } closeModal(overlayElement, () => { let result = { isConfirmed: true }; if (options.input === 'text') { result.value = inputElement ? inputElement.value : null; } if (options.descriptionInput === 'textarea') { result.description = descElement ? descElement.value : null; } if (categorySelect) { if (selectedCategoryId === NEW_CATEGORY_ID && newCategoryName) { const newCategoryId = generateCategoryId(newCategoryName); const newCategory = { id: newCategoryId, name: newCategoryName, color: generateCategoryColor(newCategoryName) }; result.category = newCategory; addCategory(newCategory); } else { result.category = categories[selectedCategoryId] || categories[OTHER_CATEGORY_ID] || { id: OTHER_CATEGORY_ID, name: 'Other', color: '#8395a7' }; } } resolve(result); }); } function handleAlertCancel(overlayElement, resolve) { closeModal(overlayElement, () => { resolve({ isConfirmed: false, value: null, description: null }); }); } function setUpKeyboardHandler(confirmHandler, cancelHandler, inputElement, descElement) { return function (event) { if (event.key === 'Escape') { cancelHandler(); } else if (event.key === 'Enter') { const activeEl = document.activeElement; if (!(activeEl === descElement && descElement?.tagName === 'TEXTAREA') && !(activeEl === inputElement && inputElement?.tagName === 'TEXTAREA')) { confirmHandler(); } else if (activeEl === inputElement && inputElement?.tagName === 'INPUT') { confirmHandler(); } } }; } function showAlert(options) { return new Promise((resolve) => { const overlay = document.createElement('div'); overlay.id = 'mz-modal-overlay'; const container = document.createElement('div'); container.id = 'mz-modal-container'; if (options.modalClass) container.classList.add(options.modalClass); const header = document.createElement('div'); header.id = 'mz-modal-header'; const titleContainer = document.createElement('div'); titleContainer.classList.add('mz-modal-title-with-icon'); const icon = createModalIcon(options.type); if (icon) titleContainer.appendChild(icon); const title = document.createElement('h2'); title.id = 'mz-modal-title'; title.textContent = options.title || ''; titleContainer.appendChild(title); header.appendChild(titleContainer); const closeButton = document.createElement('button'); closeButton.id = 'mz-modal-close'; closeButton.innerHTML = DEFAULT_MODAL_STRINGS.close; header.appendChild(closeButton); const content = document.createElement('div'); content.id = 'mz-modal-content'; if (options.htmlContent) { content.appendChild(options.htmlContent); } else if (options.text) { const textNode = document.createTextNode(options.text); content.appendChild(textNode); } let inputElem = null, descElem = null, descLabel = null, categorySelectElem = null, newCategoryInputElem = null, categoryContainer = null; if (options.input === 'text') { inputElem = document.createElement('input'); inputElem.id = 'mz-modal-input'; inputElem.type = 'text'; inputElem.value = options.inputValue || ''; inputElem.placeholder = options.placeholder || ''; } if (options.descriptionInput === 'textarea') { descLabel = document.createElement('label'); descLabel.className = 'mz-modal-label'; descLabel.textContent = options.descriptionLabel || USERSCRIPT_STRINGS.descriptionLabel; descLabel.htmlFor = 'mz-modal-description'; descElem = document.createElement('textarea'); descElem.id = 'mz-modal-description'; descElem.value = options.descriptionValue || ''; descElem.placeholder = options.descriptionPlaceholder || USERSCRIPT_STRINGS.descriptionPlaceholder; descElem.rows = 3; } if (options.showCategorySelector) { categoryContainer = document.createElement('div'); categoryContainer.className = 'category-selection-container'; const categoryLabel = document.createElement('label'); categoryLabel.className = 'category-selection-label'; categoryLabel.textContent = 'Category:'; categoryContainer.appendChild(categoryLabel); categorySelectElem = document.createElement('select'); categorySelectElem.id = 'category-selector'; const usedCategoryIds = new Set(tactics.map(t => t.style).filter(Boolean)); if (options.currentCategory) usedCategoryIds.add(options.currentCategory); const availableCategories = Object.values(categories).filter(cat => DEFAULT_CATEGORIES[cat.id] || cat.id === OTHER_CATEGORY_ID || usedCategoryIds.has(cat.id)); availableCategories.sort((a, b) => { if (a.id === OTHER_CATEGORY_ID) return 1; if (b.id === OTHER_CATEGORY_ID) return -1; return a.name.localeCompare(b.name); }); availableCategories.forEach(cat => { if (cat.id !== OTHER_CATEGORY_ID) { const opt = document.createElement('option'); opt.value = cat.id; opt.textContent = cat.name; categorySelectElem.appendChild(opt); } }); const otherOption = document.createElement('option'); otherOption.value = OTHER_CATEGORY_ID; otherOption.textContent = getCategoryName(OTHER_CATEGORY_ID); categorySelectElem.appendChild(otherOption); const addNewOption = document.createElement('option'); addNewOption.value = NEW_CATEGORY_ID; addNewOption.textContent = '+ New category'; categorySelectElem.appendChild(addNewOption); categorySelectElem.value = (options.currentCategory && categories[options.currentCategory]) ? options.currentCategory : OTHER_CATEGORY_ID; const newCategoryContainer = document.createElement('div'); newCategoryContainer.className = 'new-category-input-container'; newCategoryInputElem = document.createElement('input'); newCategoryInputElem.id = 'new-category-input'; newCategoryInputElem.type = 'text'; newCategoryInputElem.placeholder = 'New category name'; newCategoryContainer.appendChild(newCategoryInputElem); categorySelectElem.addEventListener('change', function () { const isNew = this.value === NEW_CATEGORY_ID; newCategoryContainer.classList.toggle('visible', isNew); if (isNew) newCategoryInputElem.focus(); const categoryError = document.getElementById('new-category-error'); if (categoryError) categoryError.remove(); }); categoryContainer.appendChild(categorySelectElem); categoryContainer.appendChild(newCategoryContainer); } const buttons = document.createElement('div'); buttons.id = 'mz-modal-buttons'; const confirmHandler = () => handleAlertConfirm(options, inputElem, descElem, categorySelectElem, newCategoryInputElem, overlay, resolve); const cancelHandler = () => handleAlertCancel(overlay, resolve); const confirmButton = document.createElement('button'); confirmButton.classList.add('mz-modal-btn', 'primary'); confirmButton.textContent = options.confirmButtonText || DEFAULT_MODAL_STRINGS.ok; confirmButton.addEventListener('click', confirmHandler); buttons.appendChild(confirmButton); if (options.showCancelButton) { const cancelButton = document.createElement('button'); cancelButton.classList.add('mz-modal-btn', 'cancel'); cancelButton.textContent = options.cancelButtonText || DEFAULT_MODAL_STRINGS.cancel; cancelButton.addEventListener('click', cancelHandler); buttons.appendChild(cancelButton); } closeButton.addEventListener('click', cancelHandler); const keyboardHandler = setUpKeyboardHandler(confirmHandler, cancelHandler, inputElem, descElem); document.addEventListener('keydown', keyboardHandler); container.appendChild(header); container.appendChild(content); if (inputElem) { container.appendChild(inputElem); } if (descLabel && descElem) { container.appendChild(descLabel); container.appendChild(descElem); } if (categoryContainer) { container.appendChild(categoryContainer); } container.appendChild(buttons); overlay.appendChild(container); document.body.appendChild(overlay); setTimeout(() => { overlay.classList.add('active'); if (inputElem) inputElem.focus(); else if (descElem) descElem.focus(); if (categorySelectElem && categorySelectElem.value === NEW_CATEGORY_ID) newCategoryInputElem.focus(); }, 10); overlay.addEventListener('transitionend', () => { if (!overlay.classList.contains('active')) document.removeEventListener('keydown', keyboardHandler); }); }); } function showSuccessMessage(title, text) { return showAlert({ title: title || USERSCRIPT_STRINGS.doneTitle, text: text, type: 'success' }); } function showErrorMessage(title, text) { return showAlert({ title: title || USERSCRIPT_STRINGS.errorTitle, text: text, type: 'error' }); } function showWelcomeMessage() { return showAlert({ title: '', text: USERSCRIPT_STRINGS.welcomeMessage, confirmButtonText: USERSCRIPT_STRINGS.welcomeGotIt }); } function showLoadingOverlay() { if (!loadingOverlay) { loadingOverlay = document.createElement('div'); loadingOverlay.id = 'loading-overlay'; const spinner = document.createElement('div'); spinner.id = 'loading-spinner'; loadingOverlay.appendChild(spinner); document.body.appendChild(loadingOverlay); } setTimeout(() => loadingOverlay.classList.add('visible'), 10); } function hideLoadingOverlay() { if (loadingOverlay) loadingOverlay.classList.remove('visible'); } function isFootball() { return !!document.querySelector('div#tactics_box.soccer.clearfix'); } function sha256Hash(s) { const shaObj = new jsSHA('SHA-256', 'TEXT'); shaObj.update(s); return shaObj.getHash('HEX'); } function insertAfterElement(newNode, referenceNode) { if (referenceNode && referenceNode.parentNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); } } function appendChildren(parent, children) { children.forEach((child) => { if (child) parent.appendChild(child); }); } function getFormattedDate() { const now = new Date(); const year = now.getFullYear(); const month = (now.getMonth() + 1).toString().padStart(2, '0'); const day = now.getDate().toString().padStart(2, '0'); return `${year}-${month}-${day}`; } async function fetchTacticsFromGMStorage() { return GM_getValue(FORMATIONS_STORAGE_KEY, { tactics: [] }); } function storeTacticsInGMStorage(data) { GM_setValue(FORMATIONS_STORAGE_KEY, data); } async function validateDuplicateTactic(id) { const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] }; const duplicateTactic = data.tactics.find(t => t.id === id); return duplicateTactic ? duplicateTactic.name : null; } async function saveTacticToStorage(tactic) { const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] }; data.tactics.push(tactic); await GM_setValue(FORMATIONS_STORAGE_KEY, data); } async function validateDuplicateTacticWithUpdatedCoord(newId, currentTactic, data) { if (newId === currentTactic.id) return { status: 'unchanged', name: null }; const duplicateTactic = data.tactics.find(t => t.id === newId); if (duplicateTactic) return { status: 'duplicate', name: duplicateTactic.name }; return { status: 'unique', name: null }; } function loadCompleteTacticsData() { completeTactics = GM_getValue(COMPLETE_TACTICS_STORAGE_KEY, {}); updateCompleteTacticsDropdown(); } function saveCompleteTacticsData() { GM_setValue(COMPLETE_TACTICS_STORAGE_KEY, completeTactics); } async function fetchTeamIdAndUsername(forceRefresh = false) { const now = Date.now(); const cachedInfo = GM_getValue(USER_INFO_CACHE_KEY); if (!forceRefresh && cachedInfo && cachedInfo.teamId && cachedInfo.username && (now - cachedInfo.timestamp < USER_INFO_CACHE_DURATION_MS)) { teamId = cachedInfo.teamId; username = cachedInfo.username; return { teamId, username }; } try { const usernameElement = document.getElementById('header-username'); if (!usernameElement) throw new Error('No username element found'); const currentUsername = usernameElement.textContent.trim(); const url = `/xml/manager_data.php?sport_id=1&username=${encodeURIComponent(currentUsername)}`; const response = await fetch(url); if (!response.ok) throw new Error(`HTTP error ${response.status}`); const xmlString = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); const teamElement = xmlDoc.querySelector('Team[sport="soccer"]'); if (!teamElement) throw new Error('No soccer team data found in XML'); const currentTeamId = teamElement.getAttribute('teamId'); if (!currentTeamId) throw new Error('No team ID found in XML'); teamId = currentTeamId; username = currentUsername; const newUserInfo = { teamId: teamId, username: username, timestamp: now }; GM_setValue(USER_INFO_CACHE_KEY, newUserInfo); return { teamId, username }; } catch (error) { console.error('Error fetching Team ID and Username:', error); showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Could not fetch team info. Some features might be limited.'); return { teamId: null, username: null }; } } async function fetchTeamRoster(forceRefresh = false) { const now = Date.now(); if (!teamId) { const ids = await fetchTeamIdAndUsername(); if (!ids.teamId) { console.error("Cannot fetch roster without Team ID."); return null; } } const cachedRoster = GM_getValue(ROSTER_CACHE_KEY); const isCacheValid = !forceRefresh && cachedRoster && cachedRoster.data && cachedRoster.teamId === teamId && (now - cachedRoster.timestamp < ROSTER_CACHE_DURATION_MS); if (isCacheValid) { return cachedRoster.data; } try { const url = `/xml/team_playerlist.php?sport_id=1&team_id=${teamId}`; const response = await fetch(url); if (!response.ok) throw new Error(`HTTP error ${response.status}`); const xmlString = await response.text(); const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); const playerElements = Array.from(xmlDoc.querySelectorAll('TeamPlayers Player')); const roster = playerElements.map(p => p.getAttribute('id')).filter(id => id); if (roster.length === 0) { console.warn("Fetched roster is empty for team", teamId); } rosterCache = { data: roster, timestamp: now, teamId: teamId }; GM_setValue(ROSTER_CACHE_KEY, rosterCache); return roster; } catch (error) { console.error('Error fetching team roster:', error); showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.errorFetchingRoster); return null; } } function generateUniqueId(coordinates) { coordinates.sort((a, b) => { if (a[1] !== b[1]) return a[1] - b[1]; else return a[0] - b[0]; }); const coordString = coordinates.map(coord => `${coord[0]},${coord[1]}`).join(';'); return sha256Hash(coordString); } function handleTacticSelection(tacticId) { selectedFormationTacticId = tacticId; if (!tacticId) return; const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); const selectedTactic = tactics.find(td => td.id === tacticId); if (selectedTactic) { if (outfieldPlayers.length < MIN_PLAYERS_ON_PITCH - 1) { const hiddenTrigger = document.getElementById('hidden_trigger_button'); if (hiddenTrigger) hiddenTrigger.click(); setTimeout(() => rearrangePlayers(selectedTactic.coordinates), 100); } else { rearrangePlayers(selectedTactic.coordinates); } } } function rearrangePlayers(coordinates) { const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); findBestPositions(outfieldPlayers, coordinates); for (let i = 0; i < outfieldPlayers.length; ++i) { if (coordinates[i]) { outfieldPlayers[i].style.left = coordinates[i][0] + 'px'; outfieldPlayers[i].style.top = coordinates[i][1] + 'px'; removeCollision(outfieldPlayers[i]); } } removeTacticSlotInvalidStatus(); updateFormationTextDisplay(getFormation(coordinates)); } function findBestPositions(players, coordinates) { players.sort((a, b) => parseInt(a.style.top) - parseInt(b.style.top)); coordinates.sort((a, b) => a[1] - b[1]); } function removeCollision(playerElement) { if (playerElement.classList.contains('fieldpos-collision')) { playerElement.classList.remove('fieldpos-collision'); playerElement.classList.add('fieldpos-ok'); } } function removeTacticSlotInvalidStatus() { const slot = document.querySelector(TACTIC_SLOT_SELECTOR); if (slot) slot.classList.remove('invalid'); } function updateFormationTextDisplay(formation) { const formationTextElement = document.querySelector(FORMATION_TEXT_SELECTOR); if (formationTextElement) { const defs = formationTextElement.querySelector('.defs'), mids = formationTextElement.querySelector('.mids'), atts = formationTextElement.querySelector('.atts'); if (defs) defs.textContent = formation.defenders; if (mids) mids.textContent = formation.midfielders; if (atts) atts.textContent = formation.strikers; } } function getFormation(coordinates) { let strikers = 0, midfielders = 0, defenders = 0; for (const coord of coordinates) { const y = coord[1]; if (y < 103) strikers++; else if (y <= 204) midfielders++; else defenders++; } return { strikers, midfielders, defenders }; } function getFormationFromCompleteTactic(tacticData) { let strikers = 0, midfielders = 0, defenders = 0; if (tacticData.players) { for (const pid in tacticData.players) { const player = tacticData.players[pid]; if (player.pos === 'normal' && player.initial) { const y = player.initial.y - 9; if (y < 103) strikers++; else if (y <= 204) midfielders++; else defenders++; } } } else if (tacticData.initialCoords) { const outfieldCoords = tacticData.initialCoords.filter(p => p.pos === 'normal'); for (const coord of outfieldCoords) { const y = coord.y - 9; if (y < 103) strikers++; else if (y <= 204) midfielders++; else defenders++; } } if (strikers + midfielders + defenders !== 10) { console.warn("Calculated formation from complete tactic doesn't sum to 10 outfield players."); } return { strikers, midfielders, defenders }; } function formatFormationString(formationObj) { if (!formationObj || typeof formationObj.defenders === 'undefined') return 'N/A'; return `${formationObj.defenders}-${formationObj.midfielders}-${formationObj.strikers}`; } function validateTacticPlayerCount(outfieldPlayers) { const isGoalkeeperPresent = document.querySelector(GOALKEEPER_SELECTOR); outfieldPlayers = outfieldPlayers.filter(p => !p.classList.contains('fieldpos-collision')); if (outfieldPlayers.length < MIN_PLAYERS_ON_PITCH - 1 || !isGoalkeeperPresent) { showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidTacticError); return false; } return true; } async function loadInitialTacticsAndCategories() { const initialTacticsToSave = []; if (INITIAL_FORMATIONS_DATA && INITIAL_FORMATIONS_DATA.tactics) { INITIAL_FORMATIONS_DATA.tactics.forEach(tacticEntry => { if (tacticEntry.name && Array.isArray(tacticEntry.coordinates)) { const newId = generateUniqueId(tacticEntry.coordinates); let categoryId = OTHER_CATEGORY_ID; if (DEFAULT_CATEGORIES.short_passing && tacticEntry.name.startsWith('SP_')) { categoryId = DEFAULT_CATEGORIES.short_passing.id; } else if (DEFAULT_CATEGORIES.wing_play && tacticEntry.name.startsWith('WP_')) { categoryId = DEFAULT_CATEGORIES.wing_play.id; } initialTacticsToSave.push({ name: tacticEntry.name, coordinates: tacticEntry.coordinates, id: newId, style: categoryId, description: '' }); } }); } tactics = initialTacticsToSave; tactics.sort((a, b) => a.name.localeCompare(b.name)); await GM_setValue(FORMATIONS_STORAGE_KEY, { tactics: tactics }); } function generateCategoryId(name) { return sha256Hash(name.toLowerCase()).substring(0, 10); } function generateCategoryColor(name) { const hash = sha256Hash(name); const hue = parseInt(hash.substring(0, 6), 16) % 360; const saturation = 50 + (parseInt(hash.substring(6, 8), 16) % 30); const lightness = 55 + (parseInt(hash.substring(8, 10), 16) % 15); return `hsl(${hue}, ${saturation}%, ${lightness}%)`; } function addCategory(category) { categories[category.id] = category; saveCategories(); } function saveCategories() { GM_setValue(CATEGORIES_STORAGE_KEY, categories); } function loadCategories() { const storedCategories = GM_getValue(CATEGORIES_STORAGE_KEY); if (storedCategories && typeof storedCategories === 'object') { categories = storedCategories; if (!categories.short_passing) { categories.short_passing = DEFAULT_CATEGORIES.short_passing; } if (!categories.wing_play) { categories.wing_play = DEFAULT_CATEGORIES.wing_play; } } else { categories = { ...DEFAULT_CATEGORIES }; saveCategories(); } if (!categories[OTHER_CATEGORY_ID]) { categories[OTHER_CATEGORY_ID] = { id: OTHER_CATEGORY_ID, name: 'Other', color: '#8395a7' }; } } function loadCategoryColor(categoryId) { if (categories[categoryId]) return categories[categoryId].color; else if (categoryId === 'short_passing') return DEFAULT_CATEGORIES.short_passing.color; else if (categoryId === 'wing_play') return DEFAULT_CATEGORIES.wing_play.color; else if (categoryId === OTHER_CATEGORY_ID || !categoryId) return '#8395a7'; else return '#8395a7'; } function getCategoryName(categoryId) { if (categories[categoryId]) return categories[categoryId].name; else if (categoryId === 'short_passing') return 'Short Passing'; else if (categoryId === 'wing_play') return 'Wing Play'; else if (categoryId === OTHER_CATEGORY_ID || !categoryId) return 'Other'; else return categoryId || 'Uncategorized'; } async function removeCategory(categoryId, sourceModalElement = null) { if (!categoryId || categoryId === 'all' || categoryId === OTHER_CATEGORY_ID || DEFAULT_CATEGORIES[categoryId]) { console.error("Cannot remove this category:", categoryId); return false; } const categoryName = getCategoryName(categoryId); const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.removeCategoryConfirmation.replace('{}', categoryName), showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.removeCategoryButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton, type: 'error' }); if (!confirmation.isConfirmed) return false; const data = await GM_getValue(FORMATIONS_STORAGE_KEY, { tactics: [] }); let updated = false; data.tactics = data.tactics.map(t => { if (t.style === categoryId) { t.style = OTHER_CATEGORY_ID; updated = true; } return t; }); tactics = tactics.map(t => { if (t.style === categoryId) t.style = OTHER_CATEGORY_ID; return t; }); if (updated) await GM_setValue(FORMATIONS_STORAGE_KEY, data); delete categories[categoryId]; saveCategories(); if (currentFilter === categoryId) { currentFilter = 'all'; GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter); } updateTacticsDropdown(); updateCategoryFilterDropdown(); if (sourceModalElement) { const categoryItem = sourceModalElement.querySelector(`li[data-category-id="${categoryId}"]`); if (categoryItem) { categoryItem.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; categoryItem.style.opacity = '0'; categoryItem.style.transform = 'translateX(-20px)'; setTimeout(() => categoryItem.remove(), 300); } } await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.removeCategoryAlert.replace('{}', categoryName)); return true; } async function showManagementModal() { const modalContent = createManagementModalContent(); await showAlert({ title: USERSCRIPT_STRINGS.managementModalTitle, htmlContent: modalContent, confirmButtonText: USERSCRIPT_STRINGS.manageCategoriesDoneButton, showCancelButton: false, modalClass: 'management-modal' }); updateCategoryFilterDropdown(); updateTacticsDropdown(); } function createManagementModalContent() { const wrapper = document.createElement('div'); wrapper.className = 'management-modal-wrapper'; const tabsConfig = [ { id: 'formations', title: USERSCRIPT_STRINGS.formationsTabTitle, contentGenerator: createFormationsManagementTab }, { id: 'categories', title: USERSCRIPT_STRINGS.categoriesTabTitle, contentGenerator: createCategoriesManagementTab } ]; const tabsContainer = createModalTabs(tabsConfig, wrapper); wrapper.appendChild(tabsContainer); tabsConfig.forEach((tab, index) => { const contentDiv = document.createElement('div'); contentDiv.className = 'management-modal-content'; contentDiv.dataset.tabId = tab.id; if (index === 0) contentDiv.classList.add('active'); tab.contentGenerator(contentDiv); wrapper.appendChild(contentDiv); }); wrapper.addEventListener('click', handleManagementModalClick); wrapper.addEventListener('change', handleManagementModalChange); wrapper.addEventListener('keydown', handleManagementModalKeydown); return wrapper; } function createFormationsManagementTab(container) { container.innerHTML = ''; const list = document.createElement('ul'); list.className = 'formation-management-list'; const sortedTactics = [...tactics].sort((a, b) => a.name.localeCompare(b.name)); if (sortedTactics.length === 0) { const message = document.createElement('p'); message.textContent = 'No formations saved yet.'; message.className = 'no-items-message'; list.appendChild(message); } else { sortedTactics.forEach(tactic => { list.appendChild(createFormationManagementItem(tactic)); }); } container.appendChild(list); } function createFormationManagementItem(tactic) { const listItem = document.createElement('li'); listItem.dataset.tacticId = tactic.id; const nameContainer = document.createElement('div'); nameContainer.className = 'item-name-container'; const nameSpan = document.createElement('span'); nameSpan.className = 'item-name'; nameSpan.textContent = tactic.name; nameContainer.appendChild(nameSpan); const controlsContainer = document.createElement('div'); controlsContainer.className = 'item-controls'; const categorySelect = document.createElement('select'); categorySelect.className = 'item-category-select'; populateCategorySelect(categorySelect, tactic.style); const editBtn = document.createElement('button'); editBtn.className = 'item-action-btn edit-name-btn'; editBtn.innerHTML = '✏️'; editBtn.title = 'Edit name & description'; const deleteBtn = document.createElement('button'); deleteBtn.className = 'item-action-btn delete-item-btn'; deleteBtn.innerHTML = '🗑️'; deleteBtn.title = 'Delete formation'; appendChildren(controlsContainer, [categorySelect, editBtn, deleteBtn]); appendChildren(listItem, [nameContainer, controlsContainer]); return listItem; } function populateCategorySelect(selectElement, currentCategoryId) { selectElement.innerHTML = ''; const availableCategories = Object.values(categories) .sort((a, b) => { if (a.id === OTHER_CATEGORY_ID) return 1; if (b.id === OTHER_CATEGORY_ID) return -1; return a.name.localeCompare(b.name); }); availableCategories.forEach(cat => { const option = document.createElement('option'); option.value = cat.id; option.textContent = cat.name; if (cat.id === (currentCategoryId || OTHER_CATEGORY_ID)) { option.selected = true; } selectElement.appendChild(option); }); } function createCategoriesManagementTab(container) { container.innerHTML = ''; const addCategorySection = document.createElement('div'); addCategorySection.className = 'add-category-section'; const newCategoryInput = document.createElement('input'); newCategoryInput.type = 'text'; newCategoryInput.placeholder = USERSCRIPT_STRINGS.addCategoryPlaceholder; newCategoryInput.className = 'add-category-input'; const addCategoryBtn = document.createElement('button'); addCategoryBtn.className = 'mz-modal-btn add-category-btn'; addCategoryBtn.textContent = USERSCRIPT_STRINGS.addCategoryButton; appendChildren(addCategorySection, [newCategoryInput, addCategoryBtn]); container.appendChild(addCategorySection); const list = document.createElement('ul'); list.className = 'category-management-list'; const customCategories = Object.values(categories) .filter(cat => cat.id !== OTHER_CATEGORY_ID && !DEFAULT_CATEGORIES[cat.id]) .sort((a, b) => a.name.localeCompare(b.name)); const noCatMsg = document.createElement('p'); noCatMsg.textContent = USERSCRIPT_STRINGS.noCustomCategories; noCatMsg.className = 'no-custom-categories-message'; noCatMsg.style.display = customCategories.length === 0 ? 'block' : 'none'; list.appendChild(noCatMsg); customCategories.forEach(cat => { list.appendChild(createCategoryManagementItem(cat)); }); container.appendChild(list); } function createCategoryManagementItem(category) { const listItem = document.createElement('li'); listItem.dataset.categoryId = category.id; const nameSpan = document.createElement('span'); nameSpan.textContent = category.name; nameSpan.style.flexGrow = '1'; nameSpan.style.marginRight = '10px'; const removeBtn = document.createElement('button'); removeBtn.textContent = 'Remove'; removeBtn.className = 'mz-modal-btn category-remove-btn'; removeBtn.title = `Remove category "${category.name}"`; listItem.appendChild(nameSpan); listItem.appendChild(removeBtn); return listItem; } function handleManagementModalClick(event) { const target = event.target; const listItem = target.closest('li'); if (target.classList.contains('edit-name-btn') && listItem) { handleEditFormationInModal(listItem); } else if (target.classList.contains('delete-item-btn') && listItem) { handleDeleteFormationInModal(listItem); } else if (target.classList.contains('add-category-btn')) { handleAddNewCategoryInModal(target.closest('.add-category-section')); } else if (target.classList.contains('category-remove-btn') && listItem) { handleDeleteCategoryInModal(listItem); } } function handleManagementModalChange(event) { const target = event.target; if (target.classList.contains('item-category-select')) { const listItem = target.closest('li'); const tacticId = listItem?.dataset.tacticId; const newCategoryId = target.value; if (tacticId && newCategoryId) { updateFormationCategory(tacticId, newCategoryId); } } } function handleManagementModalKeydown(event) { if (event.key === 'Enter' && !event.shiftKey) { const target = event.target; if (target.classList.contains('add-category-input')) { handleAddNewCategoryInModal(target.closest('.add-category-section')); event.preventDefault(); } } } async function handleEditFormationInModal(listItem) { const tacticId = listItem.dataset.tacticId; const tactic = tactics.find(t => t.id === tacticId); if (!tactic) return; await editTactic(tactic.id, listItem); } async function handleDeleteFormationInModal(listItem) { const tacticId = listItem.dataset.tacticId; const tactic = tactics.find(t => t.id === tacticId); if (!tactic) return; const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.deleteConfirmation.replace('{}', tactic.name), showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.deleteTacticConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton, type: 'error' }); if (!confirmation.isConfirmed) return; const deletedCategoryId = tactic.style; const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] }; data.tactics = data.tactics.filter(t => t.id !== tacticId); await GM_setValue(FORMATIONS_STORAGE_KEY, data); tactics = tactics.filter(t => t.id !== tacticId); listItem.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; listItem.style.opacity = '0'; listItem.style.transform = 'translateX(-20px)'; setTimeout(() => { listItem.remove(); const list = document.querySelector('.formation-management-list'); if (list && !list.querySelector('li')) { const message = document.createElement('p'); message.textContent = 'No formations saved yet.'; message.className = 'no-items-message'; list.appendChild(message); } }, 300); const categoryStillUsed = tactics.some(t => t.style === deletedCategoryId); if (!categoryStillUsed && deletedCategoryId && !DEFAULT_CATEGORIES[deletedCategoryId] && deletedCategoryId !== OTHER_CATEGORY_ID) { delete categories[deletedCategoryId]; saveCategories(); if (currentFilter === deletedCategoryId) { currentFilter = 'all'; GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter); } const catTab = document.querySelector('.management-modal-content[data-tab-id="categories"]'); if (catTab) createCategoriesManagementTab(catTab); } updateTacticsDropdown(); updateCategoryFilterDropdown(); } async function updateFormationCategory(tacticId, newCategoryId) { const tacticIndex = tactics.findIndex(t => t.id === tacticId); if (tacticIndex === -1) return; const originalCategoryId = tactics[tacticIndex].style; if (originalCategoryId === newCategoryId) return; tactics[tacticIndex].style = newCategoryId; const data = await GM_getValue(FORMATIONS_STORAGE_KEY, { tactics: [] }); const dataIndex = data.tactics.findIndex(t => t.id === tacticId); if (dataIndex !== -1) { data.tactics[dataIndex].style = newCategoryId; await GM_setValue(FORMATIONS_STORAGE_KEY, data); const originalCategoryStillUsed = tactics.some(t => t.style === originalCategoryId); if (!originalCategoryStillUsed && originalCategoryId && !DEFAULT_CATEGORIES[originalCategoryId] && originalCategoryId !== OTHER_CATEGORY_ID) { delete categories[originalCategoryId]; saveCategories(); if (currentFilter === originalCategoryId) { currentFilter = 'all'; GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter); } const catTab = document.querySelector('.management-modal-content[data-tab-id="categories"]'); if (catTab) createCategoriesManagementTab(catTab); } updateTacticsDropdown(); updateCategoryFilterDropdown(); } else { showErrorMessage(USERSCRIPT_STRINGS.errorTitle, "Failed to update category in storage."); tactics[tacticIndex].style = originalCategoryId; const selectElement = document.querySelector(`li[data-tactic-id="${tacticId}"] .item-category-select`); if (selectElement) selectElement.value = originalCategoryId || OTHER_CATEGORY_ID; } } async function handleAddNewCategoryInModal(addSection) { const input = addSection.querySelector('.add-category-input'); const list = addSection.nextElementSibling; if (!input || !list) return; const name = input.value.trim(); if (!name) { showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticNameProvidedError.replace("name", "category name")); input.focus(); return; } if (name.length > MAX_CATEGORY_NAME_LENGTH) { showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.categoryNameMaxLengthError); input.focus(); return; } const existingCategory = Object.values(categories).find(cat => cat.name.toLowerCase() === name.toLowerCase()); if (existingCategory) { showErrorMessage(USERSCRIPT_STRINGS.errorTitle, "Category name already exists."); input.focus(); return; } const newCategoryId = generateCategoryId(name); const newCategory = { id: newCategoryId, name: name, color: generateCategoryColor(name) }; addCategory(newCategory); input.value = ''; const noCatMsg = list.querySelector('.no-custom-categories-message'); if (noCatMsg) noCatMsg.style.display = 'none'; const newItem = createCategoryManagementItem(newCategory); list.appendChild(newItem); newItem.style.opacity = '0'; newItem.style.transform = 'translateY(-10px)'; setTimeout(() => { newItem.style.transition = 'opacity 0.3s ease, transform 0.3s ease'; newItem.style.opacity = '1'; newItem.style.transform = 'translateY(0)'; }, 10); updateCategoryFilterDropdown(); document.querySelectorAll('.item-category-select').forEach(select => { const currentTacticId = select.closest('li')?.dataset.tacticId; const currentTactic = tactics.find(t => t.id === currentTacticId); populateCategorySelect(select, currentTactic?.style); }); } async function handleDeleteCategoryInModal(listItem) { const categoryId = listItem.dataset.categoryId; const categoryName = getCategoryName(categoryId); if (!categoryId || !categoryName) return; const success = await removeCategory(categoryId, listItem.closest('.management-modal-content')); if (success) { document.querySelectorAll('.item-category-select').forEach(select => { const currentTacticId = select.closest('li')?.dataset.tacticId; const currentTactic = tactics.find(t => t.id === currentTacticId); populateCategorySelect(select, currentTactic?.style); }); const formationsTab = document.querySelector('.management-modal-content[data-tab-id="formations"]'); if (formationsTab) createFormationsManagementTab(formationsTab); } } async function addNewTactic() { const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); const coordinates = outfieldPlayers.map(p => [parseInt(p.style.left), parseInt(p.style.top)]); if (!validateTacticPlayerCount(outfieldPlayers)) return; const id = generateUniqueId(coordinates); const duplicateName = await validateDuplicateTactic(id); if (duplicateName) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.duplicateTacticErrorWithName.replace('{}', duplicateName)); return; } const result = await showAlert({ title: USERSCRIPT_STRINGS.tacticNamePrompt, input: 'text', inputValue: '', placeholder: USERSCRIPT_STRINGS.tacticNamePlaceholder, inputValidator: (v) => { if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError; if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError; if (tactics.some(t => t.name === v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError; return null; }, descriptionInput: 'textarea', descriptionValue: '', descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder, descriptionValidator: (d) => { if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError; return null; }, showCategorySelector: true, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.saveButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed || !result.value) return; const name = result.value; const description = result.description || ''; const categoryId = result.category.id; const tactic = { name: name, description: description, coordinates: coordinates, id: id, style: categoryId }; await saveTacticToStorage(tactic); tactics.push(tactic); tactics.sort((a, b) => a.name.localeCompare(b.name)); updateTacticsDropdown(id); updateCategoryFilterDropdown(); handleTacticSelection(tactic.id); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.addAlert.replace('{}', tactic.name)); } async function addNewTacticWithXml() { const xmlResult = await showAlert({ title: USERSCRIPT_STRINGS.xmlPlaceholder, input: 'text', inputValue: '', placeholder: USERSCRIPT_STRINGS.xmlPlaceholder, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.addConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!xmlResult.isConfirmed) return; const xml = xmlResult.value; if (!xml) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.xmlRequiredError); return; } if (!xml.trim().startsWith('<') || !xml.trim().endsWith('>')) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidXmlFormatError); return; } const nameResult = await showAlert({ title: USERSCRIPT_STRINGS.tacticNamePrompt, input: 'text', inputValue: '', placeholder: USERSCRIPT_STRINGS.tacticNamePlaceholder, inputValidator: (v) => { if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError; if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError; if (tactics.some(t => t.name === v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError; return null; }, descriptionInput: 'textarea', descriptionValue: '', descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder, descriptionValidator: (d) => { if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError; return null; }, showCategorySelector: true, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.saveButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!nameResult.isConfirmed || !nameResult.value) return; const name = nameResult.value; const description = nameResult.description || ''; const categoryId = nameResult.category.id; try { const newTactic = await convertXmlToSimpleFormationJson(xml, name); const id = generateUniqueId(newTactic.coordinates); const duplicateName = await validateDuplicateTactic(id); if (duplicateName) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.duplicateTacticErrorWithName.replace('{}', duplicateName)); return; } newTactic.id = id; newTactic.style = categoryId; newTactic.description = description; await saveTacticToStorage(newTactic); tactics.push(newTactic); tactics.sort((a, b) => a.name.localeCompare(b.name)); updateTacticsDropdown(id); updateCategoryFilterDropdown(); handleTacticSelection(newTactic.id); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.addAlert.replace('{}', newTactic.name)); } catch (error) { console.error('XMLError:', error); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.xmlParsingError + (error.message ? `: ${error.message}` : '')); } } async function deleteTactic() { const selectedTactic = tactics.find(t => t.id === selectedFormationTacticId); if (!selectedTactic) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); return; } const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.deleteConfirmation.replace('{}', selectedTactic.name), showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.deleteTacticConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!confirmation.isConfirmed) return; const deletedCategoryId = selectedTactic.style; const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] }; data.tactics = data.tactics.filter(t => t.id !== selectedTactic.id); await GM_setValue(FORMATIONS_STORAGE_KEY, data); tactics = tactics.filter(t => t.id !== selectedTactic.id); selectedFormationTacticId = null; const categoryStillUsed = tactics.some(t => t.style === deletedCategoryId); if (!categoryStillUsed && deletedCategoryId && !DEFAULT_CATEGORIES[deletedCategoryId] && deletedCategoryId !== OTHER_CATEGORY_ID) { delete categories[deletedCategoryId]; saveCategories(); if (currentFilter === deletedCategoryId) { currentFilter = 'all'; GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter); } } updateTacticsDropdown(); updateCategoryFilterDropdown(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.deleteAlert.replace('{}', selectedTactic.name)); } async function editTactic(tacticIdToEdit = null, sourceListItem = null) { const idToUse = tacticIdToEdit || selectedFormationTacticId; const selectedTactic = tactics.find(t => t.id === idToUse); if (!selectedTactic) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); return; } const originalName = selectedTactic.name; const originalCategory = selectedTactic.style; const originalDescription = selectedTactic.description || ''; const result = await showAlert({ title: 'Edit Formation', input: 'text', inputValue: originalName, placeholder: USERSCRIPT_STRINGS.tacticNamePlaceholder, inputValidator: (v) => { if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError; if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError; if (v !== originalName && tactics.some(t => t.name === v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError; return null; }, descriptionInput: 'textarea', descriptionValue: originalDescription, descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder, descriptionValidator: (d) => { if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError; return null; }, showCategorySelector: true, currentCategory: selectedTactic.style, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.saveButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed) return; const newName = result.value || originalName; const newDescription = result.description || ''; const newCategory = result.category?.id || originalCategory; if (newName === originalName && newCategory === originalCategory && newDescription === originalDescription) { return; } const categoryChanged = originalCategory !== newCategory; const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] }; let updatedInStorage = false; data.tactics = data.tactics.map(t => { if (t.id === selectedTactic.id) { t.name = newName; t.style = newCategory; t.description = newDescription; updatedInStorage = true; } return t; }); if (!updatedInStorage) { console.error("Failed to find tactic in storage for update.", selectedTactic.id); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, "Failed to update tactic in storage."); return; } await GM_setValue(FORMATIONS_STORAGE_KEY, data); const tacticIndex = tactics.findIndex(t => t.id === selectedTactic.id); if (tacticIndex !== -1) { tactics[tacticIndex].name = newName; tactics[tacticIndex].style = newCategory; tactics[tacticIndex].description = newDescription; } else { console.error("Failed to find tactic in memory for update.", selectedTactic.id); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, "Internal error updating tactic data."); await initializeUsData(); updateTacticsDropdown(); updateCategoryFilterDropdown(); return; } if (categoryChanged) { const originalCategoryStillUsed = tactics.some(t => t.style === originalCategory); if (!originalCategoryStillUsed && originalCategory && !DEFAULT_CATEGORIES[originalCategory] && originalCategory !== OTHER_CATEGORY_ID) { delete categories[originalCategory]; saveCategories(); if (currentFilter === originalCategory) { currentFilter = 'all'; GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter); } } } tactics.sort((a, b) => a.name.localeCompare(b.name)); updateTacticsDropdown(selectedTactic.id); updateCategoryFilterDropdown(); if (sourceListItem) { const nameSpan = sourceListItem.querySelector('.item-name'); if (nameSpan) nameSpan.textContent = newName; const categorySelect = sourceListItem.querySelector('.item-category-select'); if (categorySelect) categorySelect.value = newCategory; } await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.renameAlert.replace('{}', newName)); } async function updateTactic() { const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR)); const selectedTactic = tactics.find(t => t.id === selectedFormationTacticId); if (!selectedTactic) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); return; } if (!validateTacticPlayerCount(outfieldPlayers)) return; const updatedCoordinates = outfieldPlayers.map(p => [parseInt(p.style.left), parseInt(p.style.top)]); const newId = generateUniqueId(updatedCoordinates); const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] }; const validationResult = await validateDuplicateTacticWithUpdatedCoord(newId, selectedTactic, data); if (validationResult.status === 'unchanged') { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noChangesMadeError); return; } else if (validationResult.status === 'duplicate') { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.duplicateTacticErrorWithName.replace('{}', validationResult.name)); return; } const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.updateConfirmation.replace('{}', selectedTactic.name), showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.updateConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!confirmation.isConfirmed) return; for (const tactic of data.tactics) { if (tactic.id === selectedTactic.id) { tactic.coordinates = updatedCoordinates; tactic.id = newId; } } const memoryTactic = tactics.find(t => t.id === selectedTactic.id); if (memoryTactic) { memoryTactic.coordinates = updatedCoordinates; memoryTactic.id = newId; } await GM_setValue(FORMATIONS_STORAGE_KEY, data); selectedFormationTacticId = newId; updateTacticsDropdown(newId); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.updateAlert.replace('{}', selectedTactic.name)); } async function clearTactics() { const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.clearConfirmation, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.clearTacticsConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton, type: 'error' }); if (!confirmation.isConfirmed) return; await GM_deleteValue(FORMATIONS_STORAGE_KEY); await GM_deleteValue(OLD_FORMATIONS_STORAGE_KEY); await GM_deleteValue(CATEGORIES_STORAGE_KEY); await GM_deleteValue(CATEGORY_FILTER_STORAGE_KEY); tactics = []; selectedFormationTacticId = null; currentFilter = 'all'; loadCategories(); updateTacticsDropdown(); updateCategoryFilterDropdown(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.clearAlert); } async function resetTactics() { const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.resetConfirmation, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.resetTacticsConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton, type: 'error' }); if (!confirmation.isConfirmed) return; await GM_deleteValue(FORMATIONS_STORAGE_KEY); await GM_deleteValue(OLD_FORMATIONS_STORAGE_KEY); await GM_deleteValue(CATEGORIES_STORAGE_KEY); await GM_deleteValue(CATEGORY_FILTER_STORAGE_KEY); tactics = []; selectedFormationTacticId = null; currentFilter = 'all'; loadCategories(); await loadInitialTacticsAndCategories(); updateTacticsDropdown(); updateCategoryFilterDropdown(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.resetAlert); } async function importTacticsJsonData() { try { const result = await showAlert({ title: 'Import Formations (JSON)', input: 'text', inputValue: '', placeholder: 'Paste Formations JSON here', showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.importButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed || !result.value) return; let importedData; try { importedData = JSON.parse(result.value); } catch (e) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidImportError); return; } if (!importedData || !Array.isArray(importedData.tactics) || !importedData.tactics.every(t => t.name && t.id && Array.isArray(t.coordinates))) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidImportError); return; } const importedTactics = importedData.tactics; importedTactics.forEach(t => { if (!t.hasOwnProperty('style')) t.style = OTHER_CATEGORY_ID; if (!t.hasOwnProperty('description')) t.description = ''; if (t.style && !categories[t.style] && !DEFAULT_CATEGORIES[t.style] && t.style !== OTHER_CATEGORY_ID) { addCategory({ id: t.style, name: t.style, color: generateCategoryColor(t.style) }); } }); let existingData = await GM_getValue(FORMATIONS_STORAGE_KEY, { tactics: [] }); let existingTactics = existingData.tactics || []; const mergedTactics = [...existingTactics]; let addedCount = 0; for (const impTactic of importedTactics) { if (!existingTactics.some(t => t.id === impTactic.id)) { mergedTactics.push(impTactic); addedCount++; } else { const existingIndex = mergedTactics.findIndex(t => t.id === impTactic.id); if (existingIndex !== -1) { mergedTactics[existingIndex] = { ...mergedTactics[existingIndex], ...impTactic }; } } } await GM_setValue(FORMATIONS_STORAGE_KEY, { tactics: mergedTactics }); mergedTactics.sort((a, b) => a.name.localeCompare(b.name)); tactics = mergedTactics; updateTacticsDropdown(); updateCategoryFilterDropdown(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.importAlert + (addedCount > 0 ? ` (${addedCount} new items added)` : '')); } catch (error) { console.error('ImportError:', error); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidImportError + (error.message ? `: ${error.message}` : '')); } } async function exportTacticsJsonData() { try { const data = GM_getValue(FORMATIONS_STORAGE_KEY, { tactics: [] }); const jsonString = JSON.stringify(data, null, 2); if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(jsonString); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.exportAlert); return; } catch (clipError) { console.warn('Clipboard write failed, fallback.', clipError); } } const textArea = document.createElement('textarea'); textArea.value = jsonString; textArea.style.width = '100%'; textArea.style.minHeight = '150px'; textArea.style.marginTop = '10px'; textArea.style.backgroundColor = 'rgba(0,0,0,0.2)'; textArea.style.color = 'var(--text-color)'; textArea.style.border = '1px solid rgba(255,255,255,0.1)'; textArea.style.borderRadius = '4px'; textArea.readOnly = true; const container = document.createElement('div'); container.appendChild(document.createTextNode('Copy the JSON data:')); container.appendChild(textArea); await showAlert({ title: 'Export Formations (JSON)', htmlContent: container, confirmButtonText: 'Done' }); textArea.select(); textArea.setSelectionRange(0, 99999); } catch (error) { console.error('Export error:', error); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Failed to export formations.'); } } async function convertXmlToSimpleFormationJson(xmlString, tacticName) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); const parseErrors = xmlDoc.getElementsByTagName('parsererror'); if (parseErrors.length > 0) throw new Error(USERSCRIPT_STRINGS.xmlValidationError); const positionElements = Array.from(xmlDoc.getElementsByTagName('Pos')).filter(el => el.getAttribute('pos') === 'normal'); if (positionElements.length !== MIN_PLAYERS_ON_PITCH - 1) throw new Error(`XML must contain exactly ${MIN_PLAYERS_ON_PITCH - 1} outfield players. Found ${positionElements.length}.`); const coordinates = positionElements.map(el => { const x = parseInt(el.getAttribute('x')); const y = parseInt(el.getAttribute('y')); if (isNaN(x) || isNaN(y)) throw new Error('Invalid coordinates found in XML.'); return [x - 7, y - 9]; }); return { name: tacticName, coordinates: coordinates }; } function getAttr(element, attributeName, defaultValue = null) { return element ? element.getAttribute(attributeName) || defaultValue : defaultValue; } function parseCompleteTacticXml(xmlString) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, "text/xml"); if (xmlDoc.getElementsByTagName("parsererror").length > 0) throw new Error("XML parsing error"); const soccerTactics = xmlDoc.querySelector("SoccerTactics"); if (!soccerTactics) throw new Error("Missing <SoccerTactics>"); const teamElement = soccerTactics.querySelector("Team"); const posElements = Array.from(soccerTactics.querySelectorAll("Pos")); const subElements = Array.from(soccerTactics.querySelectorAll("Sub")); const ruleElements = Array.from(soccerTactics.querySelectorAll("TacticRule")); const data = { players: {}, substitutes: {}, teamSettings: {}, tacticRules: [], originalPlayerIDs: new Set(), description: '' }; data.teamSettings = { passingStyle: getAttr(teamElement, 'tactics', 'shortpass'), mentality: getAttr(teamElement, 'playstyle', 'normal'), aggression: getAttr(teamElement, 'aggression', 'normal'), captainPID: getAttr(teamElement, 'captain', '0') }; if (data.teamSettings.captainPID !== '0') data.originalPlayerIDs.add(data.teamSettings.captainPID); posElements.forEach(el => { const pid = getAttr(el, 'pid'); if (!pid) return; data.originalPlayerIDs.add(pid); const x = parseInt(getAttr(el, 'x', 0)); const y = parseInt(getAttr(el, 'y', 0)); data.players[pid] = { pos: getAttr(el, 'pos'), pt: getAttr(el, 'pt'), fk: getAttr(el, 'fk'), initial: { x, y }, alt1: { x: parseInt(getAttr(el, 'x1', x)), y: parseInt(getAttr(el, 'y1', y)) }, alt2: { x: parseInt(getAttr(el, 'x2', x)), y: parseInt(getAttr(el, 'y2', y)) }, }; }); subElements.forEach(el => { const pid = getAttr(el, 'pid'); if (pid) { data.originalPlayerIDs.add(pid); data.substitutes[pid] = { pid: pid, pos: getAttr(el, 'pos'), pt: getAttr(el, 'pt'), fk: getAttr(el, 'fk'), x: parseInt(getAttr(el, 'x', 0)), y: parseInt(getAttr(el, 'y', 0)), }; } }); ruleElements.forEach(el => { const rule = {}; for (const attr of el.attributes) { rule[attr.name] = attr.value; if (attr.name === 'out_player' && attr.value !== 'no_change' && attr.value !== '0') data.originalPlayerIDs.add(attr.value); if (attr.name === 'in_player_id' && attr.value !== 'NULL' && attr.value !== '0') data.originalPlayerIDs.add(attr.value); } data.tacticRules.push(rule); }); data.originalPlayerIDs = Array.from(data.originalPlayerIDs); return data; } function generateCompleteTacticXml(tacticData, playerMapping) { if (!tacticData.players) { console.error("Attempting to generate XML from an old or invalid tactic format. Operation aborted."); throw new Error("Cannot generate XML from outdated tactic format. Please resave the tactic first."); } let xml = `<?xml version="1.0" ?>\n<SoccerTactics>\n`; const mappedCaptain = playerMapping[tacticData.teamSettings.captainPID] || '0'; xml += `\t<Team tactics="${tacticData.teamSettings.passingStyle || 'shortpass'}" playstyle="${tacticData.teamSettings.mentality || 'normal'}" aggression="${tacticData.teamSettings.aggression || 'normal'}" captain="${mappedCaptain}" />\n`; for (const originalPid in tacticData.players) { const mappedPid = playerMapping[originalPid]; if (!mappedPid) continue; const p = tacticData.players[originalPid]; const initial = p.initial || { x: 0, y: 0 }; const alt1 = p.alt1 || initial; const alt2 = p.alt2 || alt1; let posTag = `\t<Pos pos="${p.pos}" pid="${mappedPid}" x="${initial.x}" y="${initial.y}" x1="${alt1.x}" y1="${alt1.y}" x2="${alt2.x}" y2="${alt2.y}"`; if (p.pt) posTag += ` pt="${p.pt}"`; if (p.fk) posTag += ` fk="${p.fk}"`; posTag += ' />\n'; xml += posTag; } for (const originalPid in tacticData.substitutes) { const mappedPid = playerMapping[originalPid]; if (!mappedPid) continue; const s = tacticData.substitutes[originalPid]; let subTag = `\t<Sub pos="${s.pos}" pid="${mappedPid}" x="${s.x}" y="${s.y}"`; if (s.pt) subTag += ` pt="${s.pt}"`; if (s.fk) subTag += ` fk="${s.fk}"`; subTag += ' />\n'; xml += subTag; } tacticData.tacticRules.forEach(rule => { const mappedOutPlayer = (rule.out_player && rule.out_player !== 'no_change') ? (playerMapping[rule.out_player] || 'no_change') : 'no_change'; const mappedInPlayer = (rule.in_player_id && rule.in_player_id !== 'NULL') ? (playerMapping[rule.in_player_id] || 'NULL') : 'NULL'; let includeRule = true; if (rule.out_player && rule.out_player !== 'no_change' && mappedOutPlayer === 'no_change') includeRule = false; if (rule.in_player_id && rule.in_player_id !== 'NULL' && mappedInPlayer === 'NULL') includeRule = false; if (includeRule) { xml += '\t<TacticRule'; for (const attr in rule) { let value = rule[attr]; if (attr === 'out_player') value = mappedOutPlayer; if (attr === 'in_player_id') value = mappedInPlayer; xml += ` ${attr}="${value}"`; } xml += ' />\n'; } }); xml += '</SoccerTactics>'; return xml; } async function saveCompleteTactic() { const exportButton = document.getElementById('export_button'); const importExportWindow = document.getElementById('importExportTacticsWindow'); const playerInfoWindow = document.getElementById('playerInfoWindow'); const importExportData = document.getElementById('importExportData'); if (!exportButton || !importExportWindow || !playerInfoWindow || !importExportData) return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Could not find required MZ UI elements for export.'); const windowHidden = importExportWindow.style.display === 'none'; if (windowHidden) { const toggleButton = document.getElementById('import_export_button'); if (toggleButton) toggleButton.click(); else return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Could not find button to toggle XML view.'); } importExportData.value = ''; exportButton.click(); await new Promise(r => setTimeout(r, 200)); const xmlString = importExportData.value; if (!xmlString) { if (windowHidden) document.getElementById('close_button')?.click(); return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Export did not produce XML.'); } let savedData; try { savedData = parseCompleteTacticXml(xmlString); } catch (error) { console.error("XML Parse Error:", error); if (windowHidden) document.getElementById('close_button')?.click(); return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.errorXmlExportParse); } const result = await showAlert({ title: USERSCRIPT_STRINGS.completeTacticNamePrompt, input: 'text', inputValue: '', placeholder: USERSCRIPT_STRINGS.completeTacticNamePlaceholder, inputValidator: (v) => { if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError; if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError; if (completeTactics.hasOwnProperty(v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError; return null; }, descriptionInput: 'textarea', descriptionValue: '', descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder, descriptionValidator: (d) => { if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError; return null; }, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.saveButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton, }); if (!result.isConfirmed || !result.value) { if (windowHidden) document.getElementById('close_button')?.click(); return; } const baseName = result.value; const description = result.description || ''; const fullName = `${baseName} (${getFormattedDate()})`; savedData.description = description; completeTactics[fullName] = savedData; saveCompleteTacticsData(); updateCompleteTacticsDropdown(fullName); if (windowHidden) document.getElementById('close_button')?.click(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticSaveSuccess.replace('{}', fullName)); } async function loadCompleteTactic() { const selectedName = selectedCompleteTacticName; if (!selectedName || !completeTactics[selectedName]) return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); showLoadingOverlay(); const originalAlert = window.alert; try { const dataToLoad = completeTactics[selectedName]; const currentRoster = await fetchTeamRoster(); if (!currentRoster) throw new Error(USERSCRIPT_STRINGS.errorFetchingRoster); const rosterSet = new Set(currentRoster); const originalPids = dataToLoad.originalPlayerIDs || []; const mapping = {}; const missingPids = []; const mappedPids = new Set(); originalPids.forEach(pid => { if (rosterSet.has(pid)) { mapping[pid] = pid; mappedPids.add(pid); } else { missingPids.push(pid); } }); const availablePids = currentRoster.filter(pid => !mappedPids.has(pid)); let replacementsFound = 0; missingPids.forEach(missingPid => { if (availablePids.length > 0) { const randomIndex = Math.floor(Math.random() * availablePids.length); const replacementPid = availablePids.splice(randomIndex, 1)[0]; mapping[missingPid] = replacementPid; replacementsFound++; } else { mapping[missingPid] = null; } }); const assignedPids = new Set(); if (dataToLoad.players) { Object.keys(dataToLoad.players).forEach(pid => { if (mapping[pid]) assignedPids.add(mapping[pid]); }); } if (dataToLoad.substitutes) { Object.keys(dataToLoad.substitutes).forEach(pid => { if (mapping[pid]) assignedPids.add(mapping[pid]); }); } if (assignedPids.size < MIN_PLAYERS_ON_PITCH) throw new Error(USERSCRIPT_STRINGS.errorInsufficientPlayers); let xmlString; try { xmlString = generateCompleteTacticXml(dataToLoad, mapping); } catch (error) { console.error("XML Gen Error:", error); throw new Error(USERSCRIPT_STRINGS.errorXmlGenerate); } let alertContent = null; window.alert = (msg) => { console.warn("Native alert captured:", msg); alertContent = msg; }; const importButton = document.getElementById('import_button'); const importExportWindow = document.getElementById('importExportTacticsWindow'); const importExportData = document.getElementById('importExportData'); if (!importButton || !importExportWindow || !importExportData) throw new Error('Could not find required MZ UI elements for import.'); const windowHidden = importExportWindow.style.display === 'none'; if (windowHidden) { document.getElementById('import_export_button')?.click(); await new Promise(r => setTimeout(r, 50)); } importExportData.value = xmlString; importButton.click(); await new Promise(r => setTimeout(r, 300)); window.alert = originalAlert; if (alertContent) throw new Error(USERSCRIPT_STRINGS.invalidXmlForImport + (alertContent.length < 100 ? ` MZ Message: ${alertContent}` : '')); const observer = new MutationObserver((mutationsList, obs) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { const errorBox = document.getElementById('lightbox_tactics_rule_error'); if (errorBox && errorBox.style.display !== 'none') { const okButton = errorBox.querySelector('#powerbox_confirm_ok_button'); if (okButton) { okButton.click(); obs.disconnect(); break; } } } } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => observer.disconnect(), 3000); if (replacementsFound > 0) { showAlert({ title: 'Warning', text: USERSCRIPT_STRINGS.warningPlayersSubstituted, type: 'info' }); } else showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticLoadSuccess.replace('{}', selectedName)); } catch (error) { console.error("Load Complete Tactic Error:", error); showErrorMessage(USERSCRIPT_STRINGS.errorTitle, error.message || 'Unknown error during load.'); if (window.alert !== originalAlert) window.alert = originalAlert; } finally { hideLoadingOverlay(); } } async function deleteCompleteTactic() { const selectedName = selectedCompleteTacticName; if (!selectedName || !completeTactics[selectedName]) return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.deleteConfirmation.replace('{}', selectedName), showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.deleteTacticConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton, type: 'error' }); if (!confirmation.isConfirmed) return; delete completeTactics[selectedName]; selectedCompleteTacticName = null; saveCompleteTacticsData(); updateCompleteTacticsDropdown(); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticDeleteSuccess.replace('{}', selectedName)); } async function editCompleteTactic() { const selectedName = selectedCompleteTacticName; if (!selectedName || !completeTactics[selectedName]) { return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); } const originalTacticData = completeTactics[selectedName]; const originalDescription = originalTacticData.description || ''; const result = await showAlert({ title: USERSCRIPT_STRINGS.renameCompleteTacticPrompt, input: 'text', inputValue: selectedName, placeholder: USERSCRIPT_STRINGS.completeTacticNamePlaceholder, inputValidator: (v) => { if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError; if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError; if (v !== selectedName && completeTactics.hasOwnProperty(v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError; return null; }, descriptionInput: 'textarea', descriptionValue: originalDescription, descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder, descriptionValidator: (d) => { if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError; return null; }, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.saveButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed || !result.value) return; const newName = result.value; const newDescription = result.description || ''; if (newName === selectedName && newDescription === originalDescription) return; const tacticData = completeTactics[selectedName]; tacticData.description = newDescription; delete completeTactics[selectedName]; completeTactics[newName] = tacticData; saveCompleteTacticsData(); selectedCompleteTacticName = newName; updateCompleteTacticsDropdown(newName); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticRenameSuccess.replace('{}', newName)); } async function updateCompleteTactic() { const selectedName = selectedCompleteTacticName; if (!selectedName || !completeTactics[selectedName]) { return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError); } const confirmation = await showAlert({ title: USERSCRIPT_STRINGS.confirmationTitle, text: USERSCRIPT_STRINGS.updateCompleteTacticConfirmation.replace('{}', selectedName), showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.updateConfirmButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!confirmation.isConfirmed) return; const originalAlert = window.alert; try { const exportButton = document.getElementById('export_button'); const importExportWindow = document.getElementById('importExportTacticsWindow'); const importExportData = document.getElementById('importExportData'); if (!exportButton || !importExportWindow || !importExportData) throw new Error('Could not find required MZ UI elements for export.'); const windowHidden = importExportWindow.style.display === 'none'; if (windowHidden) { document.getElementById('import_export_button')?.click(); await new Promise(r => setTimeout(r, 50)); } importExportData.value = ''; exportButton.click(); await new Promise(r => setTimeout(r, 200)); const xmlString = importExportData.value; if (windowHidden) document.getElementById('close_button')?.click(); if (!xmlString) throw new Error('Export did not produce XML.'); let updatedData; try { updatedData = parseCompleteTacticXml(xmlString); } catch (error) { console.error("XML Parse Error on Update:", error); throw new Error(USERSCRIPT_STRINGS.errorXmlExportParse); } updatedData.description = completeTactics[selectedName]?.description || ''; completeTactics[selectedName] = updatedData; saveCompleteTacticsData(); updateCompleteTacticsDropdown(selectedName); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticUpdateSuccess.replace('{}', selectedName)); } catch (error) { console.error("Update Complete Tactic Error:", error); showErrorMessage(USERSCRIPT_STRINGS.errorTitle, error.message || 'Unknown error during update.'); if (window.alert !== originalAlert) window.alert = originalAlert; const importExportWindow = document.getElementById('importExportTacticsWindow'); if (importExportWindow && importExportWindow.style.display !== 'none') { document.getElementById('close_button')?.click(); } } } async function importCompleteTactics() { try { const result = await showAlert({ title: USERSCRIPT_STRINGS.importCompleteTacticsTitle, input: 'text', inputValue: '', placeholder: USERSCRIPT_STRINGS.importCompleteTacticsPlaceholder, showCancelButton: true, confirmButtonText: USERSCRIPT_STRINGS.importButton, cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton }); if (!result.isConfirmed || !result.value) return; let importedData; try { importedData = JSON.parse(result.value); } catch (e) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidCompleteImportError); return; } if (typeof importedData !== 'object' || importedData === null || Array.isArray(importedData)) { await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidCompleteImportError); return; } let addedCount = 0; let updatedCount = 0; for (const name in importedData) { if (importedData.hasOwnProperty(name)) { if (typeof importedData[name] === 'object' && importedData[name] !== null) { if (!importedData[name].hasOwnProperty('description')) importedData[name].description = ''; if (!completeTactics.hasOwnProperty(name)) { addedCount++; } else { updatedCount++; } completeTactics[name] = importedData[name]; } else { console.warn(`Skipping invalid tactic data during import for key: ${name}`); } } } saveCompleteTacticsData(); updateCompleteTacticsDropdown(); let message = USERSCRIPT_STRINGS.importCompleteTacticsAlert; if (addedCount > 0 || updatedCount > 0) { message += ` (${addedCount > 0 ? `${addedCount} new` : ''}${addedCount > 0 && updatedCount > 0 ? ', ' : ''}${updatedCount > 0 ? `${updatedCount} updated` : ''} items)`; } await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, message); } catch (error) { console.error('Import Complete Tactics Error:', error); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidCompleteImportError + (error.message ? `: ${error.message}` : '')); } } async function exportCompleteTactics() { try { const jsonString = JSON.stringify(completeTactics, null, 2); if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(jsonString); await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.exportCompleteTacticsAlert); return; } catch (clipError) { console.warn('Clipboard write failed, fallback.', clipError); } } const textArea = document.createElement('textarea'); textArea.value = jsonString; textArea.style.width = '100%'; textArea.style.minHeight = '150px'; textArea.style.marginTop = '10px'; textArea.style.backgroundColor = 'rgba(0,0,0,0.2)'; textArea.style.color = 'var(--text-color)'; textArea.style.border = '1px solid rgba(255,255,255,0.1)'; textArea.style.borderRadius = '4px'; textArea.readOnly = true; const container = document.createElement('div'); container.appendChild(document.createTextNode('Copy the JSON data:')); container.appendChild(textArea); await showAlert({ title: USERSCRIPT_STRINGS.exportCompleteTacticsTitle, htmlContent: container, confirmButtonText: 'Done' }); textArea.select(); textArea.setSelectionRange(0, 99999); } catch (error) { console.error('Export Complete Tactics error:', error); await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Failed to export tactics.'); } } function createTacticPreviewElement() { if (previewElement) return previewElement; previewElement = document.createElement('div'); previewElement.id = 'mztm-tactic-preview'; previewElement.style.display = 'none'; previewElement.style.opacity = '0'; previewElement.addEventListener('mouseenter', () => { if (previewHideTimeout) clearTimeout(previewHideTimeout); }); previewElement.addEventListener('mouseleave', hideTacticPreview); document.body.appendChild(previewElement); return previewElement; } function updatePreviewPosition(event) { if (!previewElement || previewElement.style.display === 'none') return; const xOffset = 15; const yOffset = 10; let x = event.clientX + xOffset; let y = event.clientY + yOffset; const previewRect = previewElement.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; if (x + previewRect.width > viewportWidth - 10) { x = event.clientX - previewRect.width - xOffset; } if (y + previewRect.height > viewportHeight - 10) { y = event.clientY - previewRect.height - yOffset; } if (x < 10) x = 10; if (y < 10) y = 10; previewElement.style.left = `${x}px`; previewElement.style.top = `${y}px`; } function showTacticPreview(event, listItem) { if (!listItem || listItem.classList.contains('mztm-custom-select-category') || listItem.classList.contains('mztm-custom-select-no-results')) { hideTacticPreview(); return; } if (previewHideTimeout) clearTimeout(previewHideTimeout); const tacticId = listItem.dataset.tacticId; const tacticName = listItem.dataset.tacticName; const description = listItem.dataset.description || ''; const formationString = listItem.dataset.formationString || 'N/A'; const isCompleteTactic = listItem.closest('#complete_tactics_selector_list'); if (!tacticId && !tacticName) { hideTacticPreview(); return; } const previewDiv = createTacticPreviewElement(); previewDiv.innerHTML = ` <div class="mztm-preview-formation"><strong>${USERSCRIPT_STRINGS.previewFormationLabel}</strong> ${formationString}</div> ${description ? `<div class="mztm-preview-desc">${description.replace(/\n/g, '<br>')}</div>` : '<div class="mztm-preview-no-desc">No description available.</div>'} `; previewDiv.style.display = 'block'; requestAnimationFrame(() => { updatePreviewPosition(event); previewDiv.style.opacity = '1'; }); document.addEventListener('mousemove', updatePreviewPosition); } function hideTacticPreview() { if (previewHideTimeout) clearTimeout(previewHideTimeout); previewHideTimeout = setTimeout(() => { if (previewElement) { previewElement.style.opacity = '0'; setTimeout(() => { if (previewElement && previewElement.style.opacity === '0') { previewElement.style.display = 'none'; } }, 200); document.removeEventListener('mousemove', updatePreviewPosition); } previewHideTimeout = null; }, 100); } function addPreviewListenersToList(listElement) { if (!listElement) return; listElement.addEventListener('mouseover', (event) => { const listItem = event.target.closest('.mztm-custom-select-item'); if (listItem && !listItem.classList.contains('disabled')) { showTacticPreview(event, listItem); } else if (!listItem) { hideTacticPreview(); } }); listElement.addEventListener('mouseout', (event) => { const listItem = event.target.closest('.mztm-custom-select-item'); if (listItem) { const related = event.relatedTarget; if (!listItem.contains(related) && related !== previewElement) { hideTacticPreview(); } } else if (!listElement.contains(event.relatedTarget) && (!previewElement || event.relatedTarget !== previewElement)) { hideTacticPreview(); } }); window.addEventListener('scroll', hideTacticPreview, true); } function closeAllCustomDropdowns(exceptElement = null) { document.querySelectorAll('.mztm-custom-select-list-container.open').forEach(container => { const wrapper = container.closest('.mztm-custom-select-wrapper'); if (wrapper !== exceptElement?.closest('.mztm-custom-select-wrapper')) { container.classList.remove('open'); const trigger = wrapper?.querySelector('.mztm-custom-select-trigger'); trigger?.classList.remove('open'); } }); currentOpenDropdown = exceptElement?.closest('.mztm-custom-select-wrapper') || null; } document.addEventListener('click', (event) => { if (currentOpenDropdown && !currentOpenDropdown.contains(event.target)) { closeAllCustomDropdowns(); } }); function createCustomSelect(id, placeholderText) { const wrapper = document.createElement('div'); wrapper.className = 'mztm-custom-select-wrapper'; wrapper.id = `${id}_wrapper`; const trigger = document.createElement('div'); trigger.className = 'mztm-custom-select-trigger'; trigger.id = `${id}_trigger`; trigger.tabIndex = 0; const triggerText = document.createElement('span'); triggerText.className = 'mztm-custom-select-text mztm-custom-select-placeholder'; triggerText.textContent = placeholderText; trigger.appendChild(triggerText); const listContainer = document.createElement('div'); listContainer.className = 'mztm-custom-select-list-container'; listContainer.id = `${id}_list_container`; const list = document.createElement('ul'); list.className = 'mztm-custom-select-list'; list.id = `${id}_list`; listContainer.appendChild(list); addPreviewListenersToList(list); trigger.addEventListener('click', (e) => { e.stopPropagation(); const isOpen = listContainer.classList.contains('open'); closeAllCustomDropdowns(wrapper); if (!isOpen && !trigger.classList.contains('disabled')) { listContainer.classList.add('open'); trigger.classList.add('open'); currentOpenDropdown = wrapper; } }); trigger.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); trigger.click(); } }); list.addEventListener('click', (e) => { const item = e.target.closest('.mztm-custom-select-item'); if (item && !item.classList.contains('disabled')) { const value = item.dataset.value || item.dataset.tacticId || item.dataset.tacticName; const text = item.textContent; triggerText.textContent = text; triggerText.classList.remove('mztm-custom-select-placeholder'); trigger.dataset.selectedValue = value; if (id === 'tactics_selector') { handleTacticSelection(value); } else if (id === 'complete_tactics_selector') { selectedCompleteTacticName = value; } closeAllCustomDropdowns(); const changeEvent = new Event('change', { bubbles: true }); trigger.dispatchEvent(changeEvent); } }); wrapper.appendChild(trigger); wrapper.appendChild(listContainer); return wrapper; } function createTacticsSelector() { const container = document.createElement('div'); container.className = 'tactics-selector-section'; const controlsContainer = document.createElement('div'); controlsContainer.className = 'formations-controls-container'; const dropdownWrapper = createCustomSelect('tactics_selector', USERSCRIPT_STRINGS.tacticsDropdownMenuLabel); const filterControlsRow = document.createElement('div'); filterControlsRow.className = 'mztm-filter-controls-row'; const searchBox = document.createElement('input'); searchBox.type = 'text'; searchBox.className = 'tactics-search-box'; searchBox.placeholder = USERSCRIPT_STRINGS.searchPlaceholder; searchBox.addEventListener('input', (e) => { searchTerm = e.target.value.toLowerCase(); updateTacticsDropdown(selectedFormationTacticId); }); const filterDropdownWrapper = document.createElement('div'); filterDropdownWrapper.className = 'category-filter-wrapper'; const filterSelect = document.createElement('select'); filterSelect.id = 'category_filter_selector'; filterSelect.addEventListener('change', (e) => { currentFilter = e.target.value; GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter); updateTacticsDropdown(selectedFormationTacticId); }); const manageBtn = document.createElement('button'); manageBtn.id = 'manage_items_btn'; manageBtn.className = 'mzbtn manage-items-btn'; manageBtn.innerHTML = '⚙️'; manageBtn.title = USERSCRIPT_STRINGS.managementModalTitle; manageBtn.addEventListener('click', showManagementModal); filterDropdownWrapper.appendChild(filterSelect); filterDropdownWrapper.appendChild(manageBtn); appendChildren(filterControlsRow, [searchBox, filterDropdownWrapper]); appendChildren(controlsContainer, [dropdownWrapper, filterControlsRow]); container.appendChild(controlsContainer); return container; } function updateCategoryFilterDropdown() { const filterSelect = document.getElementById('category_filter_selector'); if (!filterSelect) return; filterSelect.innerHTML = ''; const usedCategoryIds = new Set(tactics.map(t => t.style || OTHER_CATEGORY_ID)); let categoriesToShow = [{ id: 'all', name: USERSCRIPT_STRINGS.allTacticsFilter }]; Object.values(categories) .filter(cat => cat.id !== 'all' && (usedCategoryIds.has(cat.id) || Object.keys(DEFAULT_CATEGORIES).includes(cat.id) || cat.id === OTHER_CATEGORY_ID)) .sort((a, b) => { if (a.id === OTHER_CATEGORY_ID) return 1; if (b.id === OTHER_CATEGORY_ID) return -1; return a.name.localeCompare(b.name); }) .forEach(cat => categoriesToShow.push({ id: cat.id, name: getCategoryName(cat.id) })); let foundCurrentFilter = false; categoriesToShow.forEach(categoryInfo => { const option = document.createElement('option'); option.value = categoryInfo.id; option.textContent = categoryInfo.name; filterSelect.appendChild(option); if (categoryInfo.id === currentFilter) { foundCurrentFilter = true; } }); if (foundCurrentFilter) { filterSelect.value = currentFilter; } else { filterSelect.value = 'all'; currentFilter = 'all'; GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter); } filterSelect.disabled = categoriesToShow.length <= 1; } function updateTacticsDropdown(currentSelectedId = null) { const listElement = document.getElementById('tactics_selector_list'); const triggerElement = document.getElementById('tactics_selector_trigger'); const triggerTextElement = triggerElement?.querySelector('.mztm-custom-select-text'); const wrapper = document.getElementById('tactics_selector_wrapper'); const searchBox = document.querySelector('.tactics-search-box'); if (!listElement || !triggerElement || !triggerTextElement || !wrapper) return; listElement.innerHTML = ''; if (searchTerm.length > 0) { wrapper.classList.add('filtering'); searchBox?.classList.add('filtering'); } else { wrapper.classList.remove('filtering'); searchBox?.classList.remove('filtering'); } const filteredTactics = tactics.filter(t => { const nameMatch = searchTerm === '' || t.name.toLowerCase().includes(searchTerm); const categoryMatch = currentFilter === 'all' || (currentFilter === OTHER_CATEGORY_ID && (!t.style || t.style === OTHER_CATEGORY_ID)) || t.style === currentFilter; return nameMatch && categoryMatch; }); const groupedTactics = {}; Object.keys(categories).forEach(id => { if (id !== 'all') groupedTactics[id] = []; }); if (!groupedTactics[OTHER_CATEGORY_ID]) groupedTactics[OTHER_CATEGORY_ID] = []; filteredTactics.forEach(t => { const categoryId = t.style || OTHER_CATEGORY_ID; if (!groupedTactics[categoryId]) { if (!groupedTactics[OTHER_CATEGORY_ID]) groupedTactics[OTHER_CATEGORY_ID] = []; groupedTactics[OTHER_CATEGORY_ID].push(t); } else groupedTactics[categoryId].push(t); }); const categoryOrder = Object.keys(groupedTactics).filter(id => groupedTactics[id].length > 0).sort((a, b) => { if (a === currentFilter) return -1; if (b === currentFilter) return 1; if (DEFAULT_CATEGORIES[a] && !DEFAULT_CATEGORIES[b]) return -1; if (!DEFAULT_CATEGORIES[a] && DEFAULT_CATEGORIES[b]) return 1; if (a === OTHER_CATEGORY_ID) return 1; if (b === OTHER_CATEGORY_ID) return -1; return (getCategoryName(a) || '').localeCompare(getCategoryName(b) || ''); }); let itemsAdded = 0; categoryOrder.forEach(categoryId => { if (groupedTactics[categoryId].length > 0) { addTacticItemsGroup(listElement, groupedTactics[categoryId], getCategoryName(categoryId), categoryId); itemsAdded += groupedTactics[categoryId].length; } }); if (itemsAdded === 0) { const noResultsItem = document.createElement('li'); noResultsItem.className = 'mztm-custom-select-no-results'; noResultsItem.textContent = tactics.length === 0 ? USERSCRIPT_STRINGS.noTacticsSaved : USERSCRIPT_STRINGS.noTacticsFound; listElement.appendChild(noResultsItem); triggerElement.classList.add('disabled'); triggerTextElement.textContent = tactics.length === 0 ? USERSCRIPT_STRINGS.noTacticsSaved : USERSCRIPT_STRINGS.tacticsDropdownMenuLabel; triggerTextElement.classList.add('mztm-custom-select-placeholder'); delete triggerElement.dataset.selectedValue; selectedFormationTacticId = null; } else { triggerElement.classList.remove('disabled'); const currentSelection = tactics.find(t => t.id === currentSelectedId); if (currentSelection) { triggerTextElement.textContent = currentSelection.name; triggerTextElement.classList.remove('mztm-custom-select-placeholder'); triggerElement.dataset.selectedValue = currentSelection.id; selectedFormationTacticId = currentSelection.id; } else { triggerTextElement.textContent = USERSCRIPT_STRINGS.tacticsDropdownMenuLabel; triggerTextElement.classList.add('mztm-custom-select-placeholder'); delete triggerElement.dataset.selectedValue; selectedFormationTacticId = null; } } } function addTacticItemsGroup(listElement, tacticsList, groupLabel, categoryId) { if (tacticsList.length === 0) return; const categoryHeader = document.createElement('li'); categoryHeader.className = 'mztm-custom-select-category'; categoryHeader.textContent = groupLabel; listElement.appendChild(categoryHeader); tacticsList.sort((a, b) => a.name.localeCompare(b.name)); tacticsList.forEach(tactic => { const item = document.createElement('li'); item.className = 'mztm-custom-select-item'; item.textContent = tactic.name; item.dataset.tacticId = tactic.id; item.dataset.value = tactic.id; item.dataset.description = tactic.description || ''; item.dataset.style = tactic.style || OTHER_CATEGORY_ID; item.dataset.formationString = formatFormationString(getFormation(tactic.coordinates)); listElement.appendChild(item); }); } function createCompleteTacticsSelector() { const container = document.createElement('div'); container.className = 'tactics-selector-section'; const label = document.createElement('label'); label.textContent = ''; label.className = 'tactics-selector-label'; const dropdownWrapper = createCustomSelect('complete_tactics_selector', USERSCRIPT_STRINGS.completeTacticsDropdownMenuLabel); container.appendChild(label); container.appendChild(dropdownWrapper); return container; } function updateCompleteTacticsDropdown(currentSelectedName = null) { const listElement = document.getElementById('complete_tactics_selector_list'); const triggerElement = document.getElementById('complete_tactics_selector_trigger'); const triggerTextElement = triggerElement?.querySelector('.mztm-custom-select-text'); const wrapper = document.getElementById('complete_tactics_selector_wrapper'); if (!listElement || !triggerElement || !triggerTextElement || !wrapper) return; listElement.innerHTML = ''; const names = Object.keys(completeTactics).sort((a, b) => a.localeCompare(b)); if (names.length === 0) { const noResultsItem = document.createElement('li'); noResultsItem.className = 'mztm-custom-select-no-results'; noResultsItem.textContent = USERSCRIPT_STRINGS.noCompleteTacticsSaved; listElement.appendChild(noResultsItem); triggerElement.classList.add('disabled'); triggerTextElement.textContent = USERSCRIPT_STRINGS.noCompleteTacticsSaved; triggerTextElement.classList.add('mztm-custom-select-placeholder'); delete triggerElement.dataset.selectedValue; selectedCompleteTacticName = null; } else { triggerElement.classList.remove('disabled'); names.forEach(name => { const tactic = completeTactics[name]; const item = document.createElement('li'); item.className = 'mztm-custom-select-item'; item.textContent = name; item.dataset.tacticName = name; item.dataset.value = name; item.dataset.description = tactic.description || ''; item.dataset.formationString = formatFormationString(getFormationFromCompleteTactic(tactic)); listElement.appendChild(item); }); const currentSelection = currentSelectedName && completeTactics[currentSelectedName] ? currentSelectedName : null; if (currentSelection) { triggerTextElement.textContent = currentSelection; triggerTextElement.classList.remove('mztm-custom-select-placeholder'); triggerElement.dataset.selectedValue = currentSelection; selectedCompleteTacticName = currentSelection; } else { triggerTextElement.textContent = USERSCRIPT_STRINGS.completeTacticsDropdownMenuLabel; triggerTextElement.classList.add('mztm-custom-select-placeholder'); delete triggerElement.dataset.selectedValue; selectedCompleteTacticName = null; } } } function createButton(id, text, clickHandler) { const button = document.createElement('button'); setUpButton(button, id, text); if (clickHandler) { button.addEventListener('click', async (e) => { e.stopPropagation(); try { await clickHandler(); } catch (err) { console.error('Button click failed:', err); showErrorMessage('Action Failed', `${err.message || err}`); } }); } return button; } async function checkVersion() { const savedVersion = GM_getValue(VERSION_KEY, null); if (!savedVersion || savedVersion !== SCRIPT_VERSION) { await showWelcomeMessage(); GM_setValue(VERSION_KEY, SCRIPT_VERSION); } } function createModeToggleSwitch() { const label = document.createElement('label'); label.className = 'mode-toggle-switch'; const input = document.createElement('input'); input.type = 'checkbox'; input.id = 'view-mode-toggle'; input.addEventListener('change', (e) => setViewMode(e.target.checked ? 'complete' : 'normal')); const slider = document.createElement('span'); slider.className = 'mode-toggle-slider'; label.appendChild(input); label.appendChild(slider); return label; } function createModeLabel(mode, isPrefix = false) { const span = document.createElement('span'); span.className = 'mode-toggle-label'; span.textContent = isPrefix ? USERSCRIPT_STRINGS.modeLabel : (mode === 'normal' ? USERSCRIPT_STRINGS.normalModeLabel : USERSCRIPT_STRINGS.completeModeLabel); span.id = `mode-label-${mode}`; return span; } function setViewMode(mode) { currentViewMode = mode; GM_setValue(VIEW_MODE_KEY, mode); const normalContent = document.getElementById('normal-tactics-content'); const completeContent = document.getElementById('complete-tactics-content'); const toggleInput = document.getElementById('view-mode-toggle'); const normalLabel = document.getElementById('mode-label-normal'); const completeLabel = document.getElementById('mode-label-complete'); const isNormal = mode === 'normal'; if (normalContent) normalContent.style.display = isNormal ? 'block' : 'none'; if (completeContent) completeContent.style.display = isNormal ? 'none' : 'block'; if (toggleInput) toggleInput.checked = !isNormal; if (normalLabel) normalLabel.classList.toggle('active', isNormal); if (completeLabel) completeLabel.classList.toggle('active', !isNormal); } function createMainContainer() { const container = document.createElement('div'); container.id = 'mz_tactics_panel'; container.classList.add('mz-panel'); const header = document.createElement('div'); header.classList.add('mz-group-main-title'); const titleContainer = document.createElement('div'); titleContainer.className = 'mz-title-container'; const titleText = document.createElement('span'); titleText.textContent = USERSCRIPT_STRINGS.managerTitle; titleText.classList.add('mz-main-title'); const versionText = document.createElement('span'); versionText.textContent = 'v' + DISPLAY_VERSION; versionText.classList.add('mz-version-text'); const modeToggleContainer = document.createElement('div'); modeToggleContainer.className = 'mode-toggle-container'; const prefixLabel = createModeLabel('', true); const modeLabelNormal = createModeLabel('normal'); const toggleSwitch = createModeToggleSwitch(); const modeLabelComplete = createModeLabel('complete'); appendChildren(modeToggleContainer, [prefixLabel, modeLabelNormal, toggleSwitch, modeLabelComplete]); appendChildren(titleContainer, [titleText, versionText, modeToggleContainer]); header.appendChild(titleContainer); const toggleButton = createToggleButton(); header.appendChild(toggleButton); container.appendChild(header); const group = document.createElement('div'); group.classList.add('mz-group'); container.appendChild(group); const normalContent = document.createElement('div'); normalContent.id = 'normal-tactics-content'; normalContent.className = 'section-content'; const tacticsSelectorSection = createTacticsSelector(); const normalButtonsSection = document.createElement('div'); normalButtonsSection.className = 'action-buttons-section'; const addCurrentBtn = createButton('add_current_tactic_btn', USERSCRIPT_STRINGS.addCurrentTactic, addNewTactic); const addXmlBtn = createButton('add_xml_tactic_btn', USERSCRIPT_STRINGS.addWithXmlButton, addNewTacticWithXml); const editBtn = createButton('edit_tactic_button', USERSCRIPT_STRINGS.renameButton, () => editTactic()); const updateBtn = createButton('update_tactic_button', USERSCRIPT_STRINGS.updateButton, updateTactic); const deleteBtn = createButton('delete_tactic_button', USERSCRIPT_STRINGS.deleteButton, deleteTactic); const importBtn = createButton('import_tactics_btn', USERSCRIPT_STRINGS.importButton, importTacticsJsonData); const exportBtn = createButton('export_tactics_btn', USERSCRIPT_STRINGS.exportButton, exportTacticsJsonData); const resetBtn = createButton('reset_tactics_btn', USERSCRIPT_STRINGS.resetButton, resetTactics); const clearBtn = createButton('clear_tactics_btn', USERSCRIPT_STRINGS.clearButton, clearTactics); const normalButtonsRow1 = document.createElement('div'); normalButtonsRow1.className = 'action-buttons-row'; appendChildren(normalButtonsRow1, [addCurrentBtn, addXmlBtn, editBtn, updateBtn, deleteBtn]); const normalButtonsRow2 = document.createElement('div'); normalButtonsRow2.className = 'action-buttons-row'; appendChildren(normalButtonsRow2, [importBtn, exportBtn, resetBtn, clearBtn]); appendChildren(normalButtonsSection, [normalButtonsRow1, normalButtonsRow2]); appendChildren(normalContent, [tacticsSelectorSection, normalButtonsSection, createHiddenTriggerButton(), createCombinedInfoButton()]); group.appendChild(normalContent); const completeContent = document.createElement('div'); completeContent.id = 'complete-tactics-content'; completeContent.className = 'section-content'; completeContent.style.display = 'none'; const completeTacticsSelectorSection = createCompleteTacticsSelector(); const completeButtonsSection = document.createElement('div'); completeButtonsSection.className = 'action-buttons-section'; const completeButtonsRow1 = document.createElement('div'); completeButtonsRow1.className = 'action-buttons-row'; const saveCompleteBtn = createButton('save_complete_tactic_button', USERSCRIPT_STRINGS.saveCompleteTacticButton, saveCompleteTactic); const loadCompleteBtn = createButton('load_complete_tactic_button', USERSCRIPT_STRINGS.loadCompleteTacticButton, loadCompleteTactic); const renameCompleteBtn = createButton('rename_complete_tactic_button', USERSCRIPT_STRINGS.renameCompleteTacticButton, editCompleteTactic); const updateCompleteBtn = createButton('update_complete_tactic_button', USERSCRIPT_STRINGS.updateCompleteTacticButton, updateCompleteTactic); const deleteCompleteBtn = createButton('delete_complete_tactic_button', USERSCRIPT_STRINGS.deleteCompleteTacticButton, deleteCompleteTactic); appendChildren(completeButtonsRow1, [saveCompleteBtn, loadCompleteBtn, renameCompleteBtn, updateCompleteBtn, deleteCompleteBtn]); const completeButtonsRow2 = document.createElement('div'); completeButtonsRow2.className = 'action-buttons-row'; const importCompleteBtn = createButton('import_complete_tactics_btn', USERSCRIPT_STRINGS.importCompleteTacticsButton, importCompleteTactics); const exportCompleteBtn = createButton('export_complete_tactics_btn', USERSCRIPT_STRINGS.exportCompleteTacticsButton, exportCompleteTactics); appendChildren(completeButtonsRow2, [importCompleteBtn, exportCompleteBtn]); appendChildren(completeButtonsSection, [completeButtonsRow1, completeButtonsRow2]); appendChildren(completeContent, [completeTacticsSelectorSection, completeButtonsSection, createCombinedInfoButton()]); group.appendChild(completeContent); return container; } function createHiddenTriggerButton() { const button = document.createElement('button'); button.id = 'hidden_trigger_button'; button.textContent = ''; button.style.cssText = 'position:absolute; opacity:0; pointer-events:none; width:0; height:0; padding:0; margin:0; border:0;'; button.addEventListener('click', function () { const presetSelect = document.getElementById('tactics_preset'); if (presetSelect) { presetSelect.value = '5-3-2'; presetSelect.dispatchEvent(new Event('change')); } }); return button; } function setUpButton(button, id, text) { button.id = id; button.classList.add('mzbtn'); button.textContent = text; } function createModalTabs(tabsConfig, modalBody) { const tabsContainer = document.createElement('div'); tabsContainer.className = 'modal-tabs'; tabsConfig.forEach((tab, index) => { const tabButton = document.createElement('button'); tabButton.className = 'modal-tab'; tabButton.textContent = tab.title; tabButton.dataset.tabId = tab.id; if (index === 0) tabButton.classList.add('active'); tabButton.addEventListener('click', () => { modalBody.querySelectorAll('.modal-tab').forEach(t => t.classList.remove('active')); modalBody.querySelectorAll('.management-modal-content, .modal-tab-content').forEach(c => c.classList.remove('active')); tabButton.classList.add('active'); const content = modalBody.querySelector(`.management-modal-content[data-tab-id="${tab.id}"], .modal-tab-content[data-tab-id="${tab.id}"]`); if (content) content.classList.add('active'); }); tabsContainer.appendChild(tabButton); }); return tabsContainer; } function createTabbedModalContent(tabsConfig) { const wrapper = document.createElement('div'); wrapper.className = 'modal-info-wrapper'; const tabs = createModalTabs(tabsConfig, wrapper); wrapper.appendChild(tabs); tabsConfig.forEach((tab, index) => { const contentDiv = document.createElement('div'); contentDiv.className = 'modal-tab-content'; contentDiv.dataset.tabId = tab.id; if (index === 0) contentDiv.classList.add('active'); const content = tab.contentGenerator(); contentDiv.appendChild(content); wrapper.appendChild(contentDiv); }); return wrapper; } function createAboutTabContent() { const content = document.createElement('div'); const aboutSection = document.createElement('div'); const aboutTitle = document.createElement('h3'); aboutTitle.textContent = 'About'; const infoText = document.createElement('p'); infoText.id = 'info_modal_info_text'; infoText.innerHTML = USERSCRIPT_STRINGS.modalContentInfoText; const feedbackText = document.createElement('p'); feedbackText.id = 'info_modal_feedback_text'; feedbackText.innerHTML = USERSCRIPT_STRINGS.modalContentFeedbackText; appendChildren(aboutSection, [aboutTitle, infoText, feedbackText]); content.appendChild(aboutSection); const faqSection = document.createElement('div'); faqSection.className = 'faq-section'; const faqTitle = document.createElement('h3'); faqTitle.textContent = 'FAQ/Function Explanations'; faqSection.appendChild(faqTitle); const formationItems = [ { q: "<code>Add Current</code> Button (Formations Mode)", a: "Saves the player positions currently visible on the pitch as a new formation. You'll be prompted for a name, category, and an optional description." }, { q: "<code>Add via XML</code> Button (Formations Mode)", a: "Allows pasting XML to add a new formation. Only player positions are saved from the XML. Prompted for name, category, and description." }, { q: "Category Filter Dropdown & <code>⚙️</code> Button (Formations Mode)", a: "Use the dropdown to filter formations by category (your last selection is remembered). Click the gear icon (⚙️) to open the Management Modal (Formations & Categories)." }, { q: "<code>Edit</code> Button (Formations Mode)", a: "Allows renaming the selected formation, changing its assigned category, and editing its description via a popup." }, { q: "<code>Update Coords</code> Button (Formations Mode)", a: "Updates the coordinates of the selected formation to match the current player positions on the pitch (description and category remain unchanged)." }, { q: "<code>Delete</code> Button (Formations Mode)", a: "Permanently removes the selected formation from the storage." }, { q: "<code>Import</code> Button (Formations Mode)", a: "Imports multiple formations from a JSON text format. Merges with existing formations (updates name/category/description if ID matches)." }, { q: "<code>Export</code> Button (Formations Mode)", a: "Exports all saved formations (including descriptions) into a JSON text format (copied to clipboard)." }, { q: "<code>Reset</code> Button (Formations Mode)", a: "Deletes all saved formations and custom categories, restoring defaults. Also resets the saved category filter." }, { q: "<code>Clear</code> Button (Formations Mode)", a: "Deletes all saved formations. Also resets the saved category filter." }, { q: "Management Modal (Gear Icon ⚙️)", a: "Opens a dedicated window to manage formations (edit name/description/category, delete) and categories (add, remove) in bulk." }, { q: "Preview on Hover (Formations Mode)", a: "Hover your mouse over a formation name in the dropdown list to see its numerical formation (e.g., 4-4-2) and its description in a small pop-up." } ]; const tacticItems = [ { q: "<code>Save Current</code> Button (Tactics Mode)", a: "Exports the entire current tactic setup (positions, alts, rules, settings) using MZ's native export, parses it, prompts for a name and description, then saves it as a new complete tactic." }, { q: "<code>Load</code> Button (Tactics Mode)", a: "Loads a saved complete tactic using MZ's native import. Shows a spinner during load. Matches players or substitutes if needed. Updates everything on the pitch." }, { q: "<code>Rename</code> Button (Tactics Mode)", a: "Allows renaming the selected complete tactic and editing its description via a popup." }, { q: "<code>Update with Current</code> Button (Tactics Mode)", a: "Overwrites the selected complete tactic's positions, rules, and settings with the setup currently on the pitch (using native export). The existing description is kept." }, { q: "<code>Delete</code> Button (Tactics Mode)", a: "Permanently removes the selected complete tactic." }, { q: "<code>Import</code> Button (Tactics Mode)", a: "Imports multiple complete tactics from a JSON text format. Merges with existing tactics, overwriting any with the same name (including description)." }, { q: "<code>Export</code> Button (Tactics Mode)", a: "Exports all saved complete tactics (including descriptions) into a JSON text format (copied to clipboard)." }, { q: "Preview on Hover (Tactics Mode)", a: "Hover your mouse over a tactic name in the dropdown list to see its numerical formation (e.g., 5-3-2, based on initial positions) and its description in a small pop-up." } ]; const combinedItems = [...formationItems, ...tacticItems].sort((a, b) => { const modeA = a.q.includes("Formations Mode") || a.q.includes("Category Filter") || a.q.includes("Management Modal") ? 0 : (a.q.includes("Tactics Mode") ? 1 : 2); const modeB = b.q.includes("Formations Mode") || b.q.includes("Category Filter") || b.q.includes("Management Modal") ? 0 : (b.q.includes("Tactics Mode") ? 1 : 2); if (modeA !== modeB) return modeA - modeB; return a.q.localeCompare(b.q); }); combinedItems.forEach(item => { const faqItemDiv = document.createElement('div'); faqItemDiv.className = 'faq-item'; const question = document.createElement('h4'); question.innerHTML = item.q; const answer = document.createElement('p'); answer.textContent = item.a; appendChildren(faqItemDiv, [question, answer]); faqSection.appendChild(faqItemDiv); }); content.appendChild(faqSection); return content; } function createLinksTabContent() { const content = document.createElement('div'); const linksSection = document.createElement('div'); const linksTitle = document.createElement('h3'); linksTitle.textContent = 'Useful Links'; const resourcesText = createUsefulContent(); const linksMap = new Map([ ['gewlaht - BoooM', 'https://www.managerzone.com/?p=forum&sub=topic&topic_id=11415137&forum_id=49&sport=soccer'], ['taktikskola by honken91', 'https://www.managerzone.com/?p=forum&sub=topic&topic_id=12653892&forum_id=4&sport=soccer'], ['peto - mix de dibujos', 'https://www.managerzone.com/?p=forum&sub=topic&topic_id=12196312&forum_id=255&sport=soccer'], ['The Zone Chile', 'https://www.managerzone.com/thezone/paper.php?paper_id=18036&page=9&sport=soccer'], ['Tactics guide by lukasz87o/filipek4', 'https://www.managerzone.com/?p=forum&sub=topic&topic_id=12766444&forum_id=12&sport=soccer&share_sport=soccer'], ['MZExtension/van.mz.playerAdvanced by vanjoge', 'https://greasyfork.org/en/scripts/373382-van-mz-playeradvanced'], ['Mazyar Userscript', 'https://greasyfork.org/en/scripts/476290-mazyar'], ['Stats Xente Userscript', 'https://greasyfork.org/en/scripts/491442-stats-xente-script'], ['More userscripts', 'https://greasyfork.org/en/users/1088808-douglasdotv'] ]); const linksList = createLinksList(linksMap); appendChildren(linksSection, [linksTitle, resourcesText, linksList]); content.appendChild(linksSection); return content; } function createCombinedInfoButton() { const button = createButton('info_button', USERSCRIPT_STRINGS.infoButton, null); button.classList.add('footer-actions'); button.style.background = 'transparent'; button.style.border = 'none'; button.style.boxShadow = 'none'; button.style.fontFamily = '"Quicksand", sans-serif'; button.style.color = 'gold'; button.addEventListener('click', (e) => { e.stopPropagation(); const tabsConfig = [{ id: 'about', title: 'About & FAQ', contentGenerator: createAboutTabContent }, { id: 'links', title: 'Useful Links', contentGenerator: createLinksTabContent }]; const modalContent = createTabbedModalContent(tabsConfig); showAlert({ title: 'MZ Tactics Manager Info', htmlContent: modalContent, confirmButtonText: DEFAULT_MODAL_STRINGS.ok }); }); return button; } function createUsefulContent() { const p = document.createElement('p'); p.id = 'useful_content'; p.textContent = USERSCRIPT_STRINGS.usefulContent; return p; } function createLinksList(linksMap) { const list = document.createElement('ul'); linksMap.forEach((href, text) => { const listItem = document.createElement('li'); const anchor = document.createElement('a'); anchor.href = href; anchor.target = '_blank'; anchor.rel = 'noopener noreferrer'; anchor.textContent = text; listItem.appendChild(anchor); list.appendChild(listItem); }); return list; } function createToggleButton() { const button = document.createElement('button'); button.id = 'toggle_panel_btn'; button.innerHTML = '✕'; button.title = 'Hide panel'; return button; } function createCollapsedIcon() { const icon = document.createElement('div'); icon.id = 'collapsed_icon'; icon.innerHTML = 'TM'; icon.title = 'Show MZ Tactics Manager'; collapsedIconElement = icon; return icon; } async function initializeUsData() { loadCategories(); currentFilter = GM_getValue(CATEGORY_FILTER_STORAGE_KEY, 'all'); const ids = await fetchTeamIdAndUsername(); if (!ids.teamId) { console.warn("Failed to get Team ID."); } tactics = []; const rawTacticDataFromStorage = GM_getValue(FORMATIONS_STORAGE_KEY); const oldRawTacticDataFromStorage = GM_getValue(OLD_FORMATIONS_STORAGE_KEY); if (!rawTacticDataFromStorage && oldRawTacticDataFromStorage && oldRawTacticDataFromStorage.tactics && Array.isArray(oldRawTacticDataFromStorage.tactics)) { console.log(`Migrating tactics from old storage key '${OLD_FORMATIONS_STORAGE_KEY}' to '${FORMATIONS_STORAGE_KEY}'.`); let migratedTactics = oldRawTacticDataFromStorage.tactics.filter(t => t && t.name && t.id && Array.isArray(t.coordinates)); migratedTactics.forEach(t => { if (!t.hasOwnProperty('style')) t.style = OTHER_CATEGORY_ID; if (!t.hasOwnProperty('description')) t.description = ''; }); tactics = migratedTactics; await GM_setValue(FORMATIONS_STORAGE_KEY, { tactics: tactics }); await GM_deleteValue(OLD_FORMATIONS_STORAGE_KEY); console.log(`Migration complete. Deleted old key '${OLD_FORMATIONS_STORAGE_KEY}'.`); } else if (!rawTacticDataFromStorage) { console.log("No existing formations data found. Loading initial default formations."); await loadInitialTacticsAndCategories(); } else { if (!rawTacticDataFromStorage.tactics || !Array.isArray(rawTacticDataFromStorage.tactics)) { rawTacticDataFromStorage.tactics = []; } let loadedTactics = rawTacticDataFromStorage.tactics.filter(t => t && t.name && t.id && Array.isArray(t.coordinates)); let dataWasChangedDuringValidation = false; loadedTactics.forEach(t => { if (!t.hasOwnProperty('style')) { t.style = OTHER_CATEGORY_ID; dataWasChangedDuringValidation = true; } if (!t.hasOwnProperty('description')) { t.description = ''; dataWasChangedDuringValidation = true; } }); if (dataWasChangedDuringValidation) { await GM_setValue(FORMATIONS_STORAGE_KEY, { tactics: loadedTactics }); } tactics = loadedTactics; } tactics.sort((a, b) => a.name.localeCompare(b.name)); loadCompleteTacticsData(); const storedCompleteTactics = GM_getValue(COMPLETE_TACTICS_STORAGE_KEY, {}); let completeTacticsChanged = false; for (const name in storedCompleteTactics) { if (storedCompleteTactics.hasOwnProperty(name)) { if (storedCompleteTactics[name] && typeof storedCompleteTactics[name] === 'object' && !storedCompleteTactics[name].hasOwnProperty('description')) { storedCompleteTactics[name].description = ''; completeTacticsChanged = true; } } } if (completeTacticsChanged) { GM_setValue(COMPLETE_TACTICS_STORAGE_KEY, storedCompleteTactics); } completeTactics = storedCompleteTactics; await checkVersion(); } function setUpTacticsInterface(mainContainer) { const toggleButton = mainContainer.querySelector('#toggle_panel_btn'); const collapsedIcon = collapsedIconElement || createCollapsedIcon(); let isCollapsed = GM_getValue(COLLAPSED_KEY, false); const anchorButtonId = 'replace-player-btn'; const applyCollapseState = (instant = false) => { const anchorButton = document.getElementById(anchorButtonId); if (collapsedIcon && collapsedIcon.parentNode) { collapsedIcon.parentNode.removeChild(collapsedIcon); } if (isCollapsed) { if (instant) { mainContainer.style.transition = 'none'; mainContainer.classList.add('collapsed'); void mainContainer.offsetHeight; mainContainer.style.transition = ''; } else { mainContainer.classList.add('collapsed'); } toggleButton.innerHTML = '☰'; toggleButton.title = 'Show panel'; if (anchorButton) { insertAfterElement(collapsedIcon, anchorButton); collapsedIcon.classList.add('visible'); } else { console.warn(`Anchor button #${anchorButtonId} not found.`); collapsedIcon.classList.remove('visible'); } } else { mainContainer.classList.remove('collapsed'); toggleButton.innerHTML = '✕'; toggleButton.title = 'Hide panel'; collapsedIcon.classList.remove('visible'); } }; applyCollapseState(true); function togglePanel() { isCollapsed = !isCollapsed; GM_setValue(COLLAPSED_KEY, isCollapsed); applyCollapseState(); } toggleButton.addEventListener('click', (e) => { e.stopPropagation(); togglePanel(); }); collapsedIcon.addEventListener('click', () => { togglePanel(); }); } async function initialize() { const tacticsBox = document.getElementById('tactics_box'); if (!tacticsBox || !isFootball()) { console.log("Tactics box not found/Invalid page."); return; } const cachedUserInfo = GM_getValue(USER_INFO_CACHE_KEY); if (cachedUserInfo && typeof cachedUserInfo === 'object' && cachedUserInfo.teamId && cachedUserInfo.username && cachedUserInfo.timestamp) { userInfoCache = cachedUserInfo; if (Date.now() - userInfoCache.timestamp < USER_INFO_CACHE_DURATION_MS) { teamId = userInfoCache.teamId; username = userInfoCache.username; } } const cachedRoster = GM_getValue(ROSTER_CACHE_KEY); if (cachedRoster && typeof cachedRoster === 'object' && cachedRoster.data && cachedRoster.timestamp) { rosterCache = cachedRoster; } try { collapsedIconElement = createCollapsedIcon(); createTacticPreviewElement(); await initializeUsData(); const mainContainer = createMainContainer(); setUpTacticsInterface(mainContainer); insertAfterElement(mainContainer, tacticsBox); updateTacticsDropdown(); updateCategoryFilterDropdown(); updateCompleteTacticsDropdown(); const savedMode = GM_getValue(VIEW_MODE_KEY, 'normal'); setViewMode(savedMode); } catch (error) { console.error(error); const errorDiv = document.createElement('div'); errorDiv.textContent = 'Error initializing MZ Tactics Manager! Check console for details.'; errorDiv.style.cssText = 'color:red; padding:10px; border:1px solid red; margin:10px;'; insertAfterElement(errorDiv, tacticsBox); } } window.addEventListener('load', initialize); })(); }).call(window, __mzcJQuery, __mzcJQuery); }); /* mzcopilot:module:[email protected] */ window.__mzcMega.run("axis", "0.4.1", function () { (function (pako, $, jQuery) { GM_addStyle(` .fieldpos::after { content: attr(data-x) " " attr(data-y); position: absolute; bottom: -14px; left: 50%; transform: translateX(-50%); font-size: 8px; color: #fff; pointer-events: none; white-space: nowrap; text-align: center; font-family: monospace; } /* error input */ .mz-error-input{ background-color:#ffd6d6; transition: background-color 0.2s ease; } `); (function() { 'use strict'; let _x = '--'; let _y = '--'; let inputX; let inputY; let formation = []; let rivalFormation = []; let rivalTeam = null; const conversionRatesToUSD = {USD: 1,R$: 2.827003,SEK: 7.4234,EUR: 0.80887,GBP: 0.555957,DKK: 6.00978,NOK: 6.921908,CHF: 1.265201,CAD: 1.3003,AUD: 1.309244,ILS: 4.378812,MXN: 10.82507,ARS: 2.807162,BOB: 7.905644,PYG: 5671.047,UYU: 28.88898,RUB: 28.21191,PLN: 3.801452,ISK: 71.15307,BGN: 1.576971,ZAR: 5.999531,THB: 43.46507,SIT: 190.539,SKK: 29.75788,JPY: 123.7233,INR: 43.66706,MZ: 7.4234,MM: 7.4234,点: 7.4234,}; if(isSoccer()) { createRivalButton(); createCoordinatesPanel(); updateCoordinatesPanel(); initPlayers(); observePitchChanges(); document.addEventListener("keydown", setKeys); } // Función para crear y configurar el botón de rival function createRivalButton() { // Obtener el primer span dentro de 'formation-container' const firstSpanOnFContainer = document.querySelector('#formation-container span'); if (!firstSpanOnFContainer) return; // Crear y configurar el botón para la táctica del rival const toAddRivalBtn = document.createElement('button'); toAddRivalBtn.id = "rivalBtn"; toAddRivalBtn.textContent = "xmlRival"; toAddRivalBtn.style.color = "blue"; // Agregar el botón al contenedor firstSpanOnFContainer.appendChild(toAddRivalBtn); // Agregar eventos toAddRivalBtn.addEventListener('click', cfgRivalFormation); toAddRivalBtn.addEventListener('mouseover', showAdditionalInfoRivalPlayers); toAddRivalBtn.addEventListener('touchstart', e => { e.preventDefault(); showAdditionalInfoRivalPlayers(); }); } function observePitchChanges() { const pitch = document.getElementById("pitch"); if (!pitch) return; const observer = new MutationObserver((mutations) => { for (const m of mutations) { // Nuevos jugadores añadidos if (m.type === "childList") { for (const node of m.addedNodes) { if (!(node instanceof HTMLElement)) continue; if (node.classList?.contains("fieldpos")) { setupPlayers(); return; } } } // Cambios en style (movimiento) if (m.type === "attributes" && m.attributeName === "style") { const el = m.target; if (el.classList?.contains("fieldpos")) { updatePlayerCoords(el); const active = getActivePlayer(); if (active === el) { setCoordsLabel({ currentTarget: active }); } } } // Cambios en class (selección) if (m.type === "attributes" && m.attributeName === "class") { const el = m.target; if (el.classList?.contains("fieldpos")) { const active = getActivePlayer(); if (!active) { _x = '--'; _y = '--'; } else { setCoordsLabel({ currentTarget: active }); attachArrowControlListeners(); } updateCoordinatesPanel(); } } } }); observer.observe(pitch, { childList: true, subtree: true, attributes: true, attributeFilter: ["style", "class"] }); } function waitForElement(selector){ return new Promise(resolve => { const el = document.querySelector(selector); if(el) return resolve(el); const observer = new MutationObserver(()=>{ const el = document.querySelector(selector); if(el){ observer.disconnect(); resolve(el); } }); observer.observe(document.body,{ childList:true, subtree:true }); }); } function attachArrowControlListeners() { const arrowControls = document.querySelectorAll('.arrow-controls'); arrowControls.forEach(control => { const buttons = control.querySelectorAll('.left-button, .right-button, .up-button, .down-button'); buttons.forEach(button => { if (!button.dataset.listenerAttached) { button.addEventListener('click', () => { const activePlayer = getActivePlayer(); if (activePlayer) { setCoordsLabel({ currentTarget: activePlayer }); } }); button.dataset.listenerAttached = "true"; } }); }); } function showAdditionalInfoRivalPlayers() { const addInfo = document.getElementsByClassName('additionalInfoRivalPlayer'); for (let i = addInfo.length - 1; i >= 0; i--) { addInfo[i].classList.toggle('hidden'); } } function initPlayers() { const container = document.getElementById('pitch'); if (!container) return; if (container.dataset.mzAxisInit) return; container.dataset.mzAxisInit = "true"; container.addEventListener('click', handlePlayerEvent); container.addEventListener('keydown', handlePlayerEvent); function handlePlayerEvent(e) { const player = e.target.closest('.fieldpos.fieldpos-ok.ui-draggable'); if (!player) return; setCoordsLabel({ currentTarget: player }); const validX = isInRange(inputX.value, 'x'); const validY = isInRange(inputY.value, 'y'); // manejar warnings validX ? removeWarning(inputX) : addWarning(inputX); validY ? removeWarning(inputY) : addWarning(inputY); } setupPlayers(); } function getActivePlayer() { return document.querySelector('.fieldpos.ui-selected'); } function getFieldPlayers() { return document.querySelectorAll('.fieldpos.fieldpos-ok.ui-draggable:not(.substitute):not(.goalkeeper)'); } function setupPlayers() { const players = document.querySelectorAll('.fieldpos.ui-draggable'); players.forEach(player => { // evitar reinicializar if (player.dataset.initialized) return; player.dataset.initialized = "true"; updatePlayerCoords(player); $(player).draggable({ drag: function(event, ui) { const player = this; const pos = pitchToCoords(ui.position.left, ui.position.top); player.dataset.x = pos.x; player.dataset.y = pos.y; _x = pos.x; _y = pos.y; updateCoordinatesPanel(); } }); let originalStop = $(player).draggable("option", "stop"); $(player).draggable("option", "stop", function(event, ui) { if (originalStop) originalStop.call(this, event, ui); updatePlayerCoords(player); _x = player.dataset.x; _y = player.dataset.y; updateCoordinatesPanel(); }); }); } function setCoordsLabel(event) { const player = event.currentTarget; getOffset(player); updateCoordinatesPanel(); } function getOffset( el ) { const pos = pitchToCoords(el.offsetLeft, el.offsetTop); _x = pos.x; _y = pos.y; } function updateCoordinatesPanel(){ inputX.value = _x; inputY.value = _y; const disabled = (_x === '--'); inputX.disabled = disabled; inputY.disabled = disabled; } function copyFormation() { const coords = getCurrentCoords(); if(!coords) return; const txtCoord = coords.map(p => `${p.x},${p.y}`).join("\n"); navigator.clipboard.writeText(txtCoord) .then(() => { console.log('Formation copied to the clipboard'); alert('Formation copied to the clipboard'); }) .catch(err => { console.error('Error copying to the clipboard:', err); }) //console.log(txtCoord); } function getCurrentCoords(){ const players = [...getFieldPlayers()]; if(players.length !== 10){ alert("Not Valid Formation"); return null; } const coords = players.map(p => ({ x: Number(p.dataset.x), y: Number(p.dataset.y) })); // ordenar por Y (arriba→abajo) y luego por X (izquierda→derecha) coords.sort((a,b)=> a.y - b.y || a.x - b.x); return coords; } function saveTactic(){ const coords = getCurrentCoords(); if(!coords) return; const name = prompt("Tactic name:"); if(!name) return; const text = name + "\n" + coords.map(p => `${p.x},${p.y}`).join("\n"); showNote(2026,'01','01',1); setTimeout(()=>{ const textarea = document.querySelector("textarea[name='cal_message']"); if(!textarea) return; textarea.value = text; $("#calendar_add_not_btn").triggerHandler("click"); },500); } async function loadTactics(){ $("#calendar-new").datepicker("setDate", new Date(2026,0,1)); setTimeout(async ()=>{ [...document.querySelectorAll("#calendar-new td[data-handler='selectDay']")] .find(td => td.textContent.trim() === "1") .click(); await waitForElement("textarea[id^='note']"); const notes = [...document.querySelectorAll("textarea[id^='note']")]; const tactics = []; notes.forEach(n => { const noteID = n.id.replace("note",""); const lines = n.value .split("\n") .map(l=>l.trim()) .filter(Boolean); if(lines.length >= 11){ const name = lines[0]; const coords = lines.slice(1,11); tactics.push({ name, coords, noteID }); } }); powerboxClose('calendar-administration'); showTacticsModal(tactics); },300); } function showTacticsModal(tactics){ let modal = document.getElementById("mzTacticModal"); if(modal) modal.remove(); modal = document.createElement("div"); modal.id = "mzTacticModal"; modal.style.position = "fixed"; modal.style.top = "120px"; modal.style.left = "50%"; modal.style.transform = "translateX(-50%)"; modal.style.background = "#f2f2f2"; modal.style.border = "1px solid #666"; modal.style.padding = "10px"; modal.style.zIndex = "9999"; modal.style.minWidth = "220px"; modal.style.maxHeight = "70vh"; modal.style.overflowY = "auto"; let html = `<div style="font-weight:bold;margin-bottom:8px;">Tactics Library</div>`; tactics.forEach((t,i)=>{ html += ` <div style="display:flex;justify-content:space-between; padding:4px;border-bottom:1px solid #ddd;"> <span class="mzTacticItem" data-index="${i}" style="cursor:pointer;"> ${t.name} </span> <span class="mzDeleteTactic" data-index="${i}" style="cursor:pointer;color:red;"> 🗑 </span> </div>`; }); html += ` <div style="text-align:center;margin-top:8px;"> <button id="mzCloseTacticModal">Close</button> </div> `; modal.innerHTML = html; document.body.appendChild(modal); modal.querySelectorAll(".mzTacticItem").forEach(el=>{ el.onclick = function(){ const t = tactics[this.dataset.index]; loadTactic(t); modal.remove(); }; }); modal.querySelectorAll(".mzDeleteTactic").forEach(el=>{ el.onclick = function(){ const t = tactics[this.dataset.index]; if(!confirm("Delete tactic: "+t.name+" ?")) return; confirm_delete_note( t.noteID, '2026-01-01 00:00:00', 2026, '01', 1 ); modal.remove(); setTimeout(()=>{ powerboxClose('calendar-administration'); loadTactics(); // recargar lista },400); }; }); document.getElementById("mzCloseTacticModal") .onclick = ()=> modal.remove(); } function loadTactic(tactic){ formation = []; tactic.coords.forEach(c => { let [x,y] = c.split(","); const pos = coordsToPitch(Number(x), Number(y)); formation.push([ pos.left, pos.top ]); }); // Optional because it’s already saved in sorted order formation = sortFormation(formation); setFormation(); } function sortFormation(arr){ return arr.sort((a,b) => b[1] - a[1] || a[0] - b[0]); } function pasteFormation() { navigator.clipboard.readText() .then(text => { console.log('Clipboard text:', text) // is xml content? if (text.search("xml") != -1) { // parse clipboard to xml file console.log('Cartesian coordinate system from XML file'); let parser = new DOMParser(); let xmlDoc = parser.parseFromString(text,"text/xml"); formation = []; let pos = xmlDoc.getElementsByTagName("Pos") for (let i = 1; i < pos.length; i++) { formation.push([Number(pos[i].getAttribute("x")) - 7, Number(pos[i].getAttribute("y")) - 9]); } formation = sortFormation(formation); // formation.sort(function(a,b) { if (b[1] == a[1]) return a[0] - b[0]; else return b[1] - a[1]; }) setFormation(); } else if (text.trim().split('\n').length == 10) { // is there 10 coord? console.log('Cartesian coordinate system from Clipboard'); let coorXY = text.trim().split('\n') formation = []; for (let pair of coorXY) { let [xAxis,yAxis] = pair.split(','); const pos = coordsToPitch( Number(xAxis), Number(yAxis) ); formation.push([ pos.left, pos.top]); // formation.push([Number(xAxis) + 97, 155 - Number(yAxis)]); } formation = sortFormation(formation); //formation.sort(function(a,b) { if (b[1] == a[1]) return a[0] - b[0]; else return b[1] - a[1]; }) setFormation(); } else if (formation.length == 10) { console.log('Cartesian coordinate system from internal var'); setFormation(); } else { console.log("Nothing to do"); } }) .catch(err => { console.error('Error reading from the clipboard:', err) }) } function setFormation() { let player = getFieldPlayers(); if (formation.length == player.length) { // get an order let oldFormation = []; for (let i = 0; i < player.length; i++) { oldFormation.push([Number((player[i].style.left).slice(0,-2)), Number((player[i].style.top).slice(0,-2)), i]); } oldFormation = sortFormation(oldFormation); //oldFormation.sort(function(a,b) { if (b[1] == a[1]) return a[0] - b[0]; else return b[1] - a[1]; }) for (let i = 0; i < formation.length; i++) { player[oldFormation[i][2]].style.left = formation[i][0] + 'px'; player[oldFormation[i][2]].style.top = formation[i][1] + 'px'; } alert('Formation successfully copied'); formation = []; // empty formation } } function cfgRivalFormation() { let elBoton = document.getElementById('rivalBtn'); if(elBoton.innerHTML == "xmlRival") { pasteRivalFormation(); } else { elBoton.style.color = "blue"; elBoton.innerHTML = "xmlRival"; let rivalPlayers = document.getElementById('rivalPlayers'); if(rivalPlayers) { rivalPlayers.parentElement.removeChild(rivalPlayers); } navigator.clipboard.writeText('') .then(() => { console.log('Clipboard Clear'); }) .catch(err => { console.error('Error clearing the clipboard:', err); }) } } function pasteRivalFormation(){ navigator.clipboard.readText() .then(text => { // is xml content? if (text.search("xml") != -1) { console.log('Clipboard text:', text) // parse clipboard to xml file console.log('Cartesian coordinate system from XML file'); let parser = new DOMParser(); let xmlDoc = parser.parseFromString(text,"text/xml"); rivalFormation = []; let pos = xmlDoc.getElementsByTagName("Pos") // x, y, shirtno, value, height, weight, age, countryShortname for (let i = 0; i < pos.length; i++) { rivalFormation.push([Number(pos[i].getAttribute("x")) - 7, Number(pos[i].getAttribute("y")) - 9, '9', '', '', '', '', '' ]); } rivalFormation = sortFormation(rivalFormation); // rivalFormation.sort(function(a,b) { if (b[1] == a[1]) return a[0] - b[0]; else return b[1] - a[1]; }) rivalTeam = xmlDoc.querySelector("Team"); rivalTeam.setAttribute("tactic", rivalTeam.getAttribute('tactics')); setRivalFormation(); } else if (text != null && text != "") { // is valid id number let matchId = prompt("Please enter your Match ID [mid]:", (text.trim().length < 14 && !isNaN(text.trim()))? text.trim():"No IDea"); if (matchId == null) { console.log("User cancel this"); } else if(matchId == "" || matchId == "No IDea" || matchId.length < 5 || matchId.length > 14 || isNaN(matchId.trim()) ) { console.log("User dont have any idea about this or not valid match id"); } else { matchId = matchId.trim(); console.log(`Your matchId is:${matchId}`); getDataFromXML(matchId); } } else if (rivalFormation.length > 0) { console.log('Formation from system on internal var'); setRivalFormation(); } else { console.log("Nothing to do"); } }) .catch(err => { console.error('Error reading from the clipboard:', err) }) } function getDataFromXML(matchId) { let statsUrl = `http://download06.managerzone.com/data/soccer/${matchId.slice(-1)}/${matchId.charAt(matchId.length-2)}/stats${matchId}.xml.gz`; GM_xmlhttpRequest({ method: 'GET', // url: 'http://download06.managerzone.com/data/soccer/7/1/stats1486585817.xml.gz', url: statsUrl, responseType: 'arraybuffer', // We want to handle the binary data (arraybuffer) onload: function(response) { if (response.status === 200) { try { // Decompress the .gz file using pako const decompressed = pako.ungzip(new Uint8Array(response.response), { to: 'string' }); // Parse the XML data const parser = new DOMParser(); const xmlDoc = parser.parseFromString(decompressed, 'text/xml'); let teams = xmlDoc.documentElement.getElementsByTagName("Team"); let ishome = 0; if (confirm(`Press [ OK ] if your rival is:\t${teams[0].getAttribute('name')}\nPress [ Cancel ] if your rival is:\t${teams[1].getAttribute('name')}`)) { ishome = 1; } else { ishome = 0; } rivalTeam = ishome ? teams[0] : teams[1]; console.log(rivalTeam.getAttribute('name')); rivalFormation = []; // x, y, shirtno, value, height, weight, age, countryShortname let players = xmlDoc.querySelectorAll(`Player[teamId="${rivalTeam.getAttribute('id')}"][origin*=","]`); for (let i = 0; i < players.length; i++) { let origin = players[i].getAttribute('origin'); let xy = origin.split(","); rivalFormation.push([StatsToPos_X(xy[0], ishome) - 7, StatsToPos_Y(xy[1], ishome) - 9, players[i].getAttribute('shirtno'), '', '', '', '', '' ]); } // Additional Data let squadTeamUrl = `http://www.managerzone.com/xml/team_playerlist.php?sport_id=1&team_id=${rivalTeam.getAttribute('id')}` GM_xmlhttpRequest({ method: 'GET', url: squadTeamUrl, onload: function(response) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(response.responseText, "text/xml"); let teamPlayers = xmlDoc.querySelector('TeamPlayers'); for (let i = 0; i < players.length; i++) { let player = xmlDoc.querySelector(`Player[id="${players[i].getAttribute('id')}"]`); if (player) { let value = Number(player.getAttribute('value')) / (conversionRatesToUSD[teamPlayers.getAttribute('teamCurrency')] || 1) rivalFormation[i][3] = '' + value.toFixed(0); //player.getAttribute('value').replace(/\B(?=(\d{3})+(?!\d))/g, ' '); rivalFormation[i][4] = player.getAttribute('height'); rivalFormation[i][5] = player.getAttribute('weight'); rivalFormation[i][6] = player.getAttribute('age'); rivalFormation[i][7] = player.getAttribute('countryShortname'); } } setRivalFormation(); }, onerror: function(error) { console.log('Error:', error); } }); } catch (error) { console.error('Error decompressing or parsing XML:', error); } } else { console.error('Failed to fetch the file:', response.statusText); alert('The match has probably not been viewed in 2D yet, therefore we cannot obtain the statistics, or try again later.'); } }, onerror: function(error) { console.error('Error with GM_xmlhttpRequest:', error); } }); } function StatsToPos_X (i, isHome) { let ret = isHome ? Math.round(-.255800462 * i + 199.8228530689) : Math.round(.2555000556 * i + 8.3741302936); return ret; } function StatsToPos_Y (i, isHome) { let ret = isHome ? Math.round(-.3073207154 * i + 315.9278777381) : Math.round(.3070644902 * i + 9.2794889414); return ret; } function setRivalFormation () { let rivalPlayers = document.createElement('div'); rivalPlayers.id = "rivalPlayers"; let formationText = document.getElementById('formation_text'); formationText.parentNode.insertBefore(rivalPlayers, formationText.nextSibling); console.log('players on the pitch'); let contentRival = ''; if (rivalTeam.hasAttribute("name")) { contentRival += `<div style="position:absolute; left: 1px; top: 3px; width: 215px;">`+ `<div style="display: flex; justify-content: space-between; font-size:0.7em; color:black"><div style="display: flex; flex-direction: column; justify-content: space-around; max-width: 90px; text-align: center;"><span>${rivalTeam.getAttribute('name')}</span><span><img src="https://www.managerzone.com/nocache-910/img/flags/64/${rivalTeam.getAttribute('country').toLowerCase()}.png" width="14" alt=""></span></div>` + `<span><img src="https://www.managerzone.com/dynimg/badge.php?team_id=${rivalTeam.getAttribute('id')}" width="25" alt=""></span></div></div>`; } contentRival += `<div style="position:absolute; left: 1px; top: 44px; width: 215px">` + `<div style="display: flex; justify-content: space-around; font-size:0.7em;"><span>${rivalTeam.getAttribute('tactic')}</span><span>${rivalTeam.getAttribute('playstyle')}</span><span>${rivalTeam.getAttribute('aggression')}</span></div></div>`; for (let i = 0; i < rivalFormation.length; i++) { contentRival += "<div style=\"position:absolute; left: " + (194 - rivalFormation[i][0]) + "px; top: " + (310 - rivalFormation[i][1]) + "px;\">" + "<div style=\"border-radius: 50%; width: 18px; height: 18px; border: 2px solid #FFE42B; color: black; display: flex; align-items: center; justify-content: center; font-size:0.9em; font-weight: bold;\">"+rivalFormation[i][2]+"</div>" + "<div style=\"position:absolute; top: -6px; width:22px; display: flex; justify-content: space-between; font-size:0.45em; color:black\"><span>" + (rivalFormation[i][0] - 97) + "</span><span>" + (155 - rivalFormation[i][1]) + "</span></div>" + `<div class="additionalInfoRivalPlayer hidden"><div style="position: absolute; white-space: nowrap; font-size:0.45em; writing-mode: vertical-rl; text-align: right; color:black; left: 22px; top: -6px; height:37px"><span>${rivalFormation[i][3]?rivalFormation[i][3].replace(/\B(?=(\d{3})+(?!\d))/g, ' ')+" USD":''}</span></div>` + `<div style="display: flex; justify-content: space-between; font-size:0.3em; color:black"><span>${rivalFormation[i][4]?rivalFormation[i][4]+" cm":''}</span><span>${rivalFormation[i][6]?rivalFormation[i][6]+" y":''}</span></div>` + `<div style="display: flex; justify-content: space-between; font-size:0.3em; color:black"><span>${rivalFormation[i][5]?rivalFormation[i][5]+" kg":''}</span><span>${rivalFormation[i][7]?`<img src="https://www.managerzone.com/nocache-910/img/flags/s_${rivalFormation[i][7]}.gif" width="7" alt="">`:''}</span></div></div></div>`; } rivalPlayers.innerHTML = contentRival; let elBoton = document.getElementById('rivalBtn'); elBoton.style.color = '#FF4500'; elBoton.innerHTML = "clearRival"; } function createCoordinatesPanel(){ if (document.getElementById("divCoords")) return; const coordsContainer = document.getElementById('formation-container'); if (!coordsContainer) return; const div = document.createElement("div"); div.id = "divCoords"; div.innerHTML = ` <span style="font-weight:600"> Coords: <span style="color:green">X</span> <input id="inputX" type="text" style="width:24px"> <span style="color:blue">Y</span> <input id="inputY" type="text" style="width:24px"> </span> <button id="copyBtn">Copy</button> <button id="pasteBtn">Paste</button> `; coordsContainer.appendChild(div); const copyBtn = div.querySelector("#copyBtn"); const pasteBtn = div.querySelector("#pasteBtn"); inputX = div.querySelector("#inputX"); inputY = div.querySelector("#inputY"); inputX.inputMode = "numeric"; inputY.inputMode = "numeric"; inputX.addEventListener("input", setPlayerPosition); inputY.addEventListener("input", setPlayerPosition); const selectAll = e => e.target.select(); inputX.addEventListener("focus", selectAll); inputY.addEventListener("focus", selectAll); copyBtn.addEventListener("click", copyFormation); pasteBtn.addEventListener("click", pasteFormation); [copyBtn, pasteBtn].forEach(btn => { btn.style.display = "inline-block"; btn.style.margin = "0 0px"; btn.style.padding = "2px 3px"; // más pequeño en móviles btn.style.boxSizing = "border-box"; }); } function setPlayerPosition() { const x = Number(inputX.value); const y = Number(inputY.value); const player = getActivePlayer(); if(!player) return; const validX = isInRange(x, 'x'); const validY = isInRange(y, 'y'); validX ? removeWarning(inputX) : addWarning(inputX); validY ? removeWarning(inputY) : addWarning(inputY); if(validX && validY){ const pos = coordsToPitch(x, y); player.style.left = pos.left + "px"; player.style.top = pos.top + "px"; } } function isInRange(number, coordinate) { const n = Number(number); if (Number.isNaN(n)) return false; if (coordinate === 'x') { return n >= -96 && n <= 96; } if (coordinate === 'y') { return n >= -157 && n <= 101; } return false; } function setKeys(key) { if((key.keyCode === 37 || key.keyCode === 38 || key.keyCode === 39 || key.keyCode === 40) && (key.currentTarget.activeElement.localName != 'input')) { const players = document.querySelectorAll('.fieldpos.fieldpos-ok.ui-draggable.ui-selected, .fieldpos.ui-selected.fieldpos-collision'); if(players.length) { const pos = pitchToCoords(players[0].offsetLeft, players[0].offsetTop); _x = pos.x; _y = pos.y; } else { _y = '--'; _x = '--'; } updateCoordinatesPanel(); } } function updatePlayerCoords(player){ const pos = pitchToCoords(player.offsetLeft, player.offsetTop); player.dataset.x = pos.x; player.dataset.y = pos.y; } function coordsToPitch(x, y){ return { left: x + 97, top: 155 - y }; } function pitchToCoords(left, top){ return { x: Math.round(left) - 97, y: 155 - Math.round(top) }; } function addWarning(input) { input.classList.add("mz-error-input"); } function removeWarning(input) { input.classList.remove("mz-error-input"); } function isSoccer() { const sport = document.getElementById('tactics_box'); return sport?.classList.contains('soccer') ?? false; } })(); }).call(window, __mzcPako2, __mzcJQuery, __mzcJQuery); }); /* mzcopilot:module:[email protected] */ window.__mzcMega.run("training-history", "7.2", function () { (function () { (() => { 'use strict'; const SKILL_MAP = { '1': 'Speed', '2': 'Stamina', '3': 'Play Intelligence', '4': 'Passing', '5': 'Shooting', '6': 'Heading', '7': 'Keeping', '8': 'Ball Control', '9': 'Tackling', '10': 'Aerial Passing', '11': 'Set Plays' }; const ORDERED_SKILLS = ['Speed', 'Stamina', 'Play Intelligence', 'Passing', 'Shooting', 'Heading', 'Keeping', 'Ball Control', 'Tackling', 'Aerial Passing', 'Set Plays']; const CURRENCIES = { "R$": 2.62589, EUR: 9.1775, USD: 7.4234, "点": 1, SEK: 1, NOK: 1.07245, DKK: 1.23522, GBP: 13.35247, CHF: 5.86737, RUB: 0.26313, CAD: 5.70899, AUD: 5.66999, MZ: 1, MM: 1, PLN: 1.95278, ILS: 1.6953, INR: 0.17, THB: 0.17079, ZAR: 1.23733, SKK: 0.24946, BGN: 4.70738, MXN: 0.68576, ARS: 2.64445, BOB: 0.939, UYU: 0.256963, PYG: 0.001309, ISK: 0.10433, SIT: 0.03896, JPY: 0.06, }; const SPECIAL_CHIP_ICONS = { 'availability chip': 'availability', 'time saver chip': 'time_saver', 'efficiency chip': 'efficiency', 'freebie chip': 'freebie' }; let skillTranslations = {}; let insensitiveSkillTranslations = {}; let myTeamId = null; let preferredCurrency = GM_getValue('PREFERRED_CURRENCY', 'USD'); let comparisonChartInstances = {}; try { GM_addStyle(GM_getResourceText('trainingHistoryStyles')); const translationsCode = GM_getResourceText('skillTranslationsJS'); if (!translationsCode) { throw new Error("Could not load tr resource."); } const toBeRun = translationsCode.replace(/^export\s+/gm, ''); const getTranslations = new Function(`${toBeRun}; return { SKILL_TRANSLATIONS, insensitiveSkillTranslations };`); const loadedTranslations = getTranslations(); skillTranslations = loadedTranslations.SKILL_TRANSLATIONS || {}; insensitiveSkillTranslations = loadedTranslations.insensitiveSkillTranslations || {}; if (Object.keys(skillTranslations).length === 0) { throw new Error("Could not load tr resource."); } if (Object.keys(insensitiveSkillTranslations).length === 0) { if(Object.keys(skillTranslations).length > 0) { insensitiveSkillTranslations = Object.fromEntries( Object.entries(skillTranslations).map(([key, value]) => [key.toLowerCase(), value]) ); } else { throw new Error("Could not create tr map."); } } } catch (_err) { return; } const getEnglishSkillName = (nativeName) => { if (!nativeName) return ''; return insensitiveSkillTranslations[nativeName.toLowerCase()] || nativeName; }; const isClubMember = () => { const headerUsernameStyle = document.querySelector('#header-username')?.getAttribute('style'); return headerUsernameStyle && headerUsernameStyle.includes('background-image'); }; const canRunUserscript = () => isClubMember(); const getCurrentSeasonInfo = () => { const w = document.querySelector('#header-stats-wrapper'); if (!w) return null; const dn = w.querySelector('h5.flex-grow-1.textCenter:not(.linked)'); const ln = w.querySelector('h5.flex-grow-1.textCenter.linked'); if (!dn || !ln) return null; const dm = dn.textContent.match(/(\d{1,2})[/-](\d{1,2})[/-](\d{4})/); if (!dm) return null; const d = dm[1]; const m = dm[2]; const y = dm[3]; const currentDate = new Date([m, d, y].join('/')); const digits = ln.textContent.match(/\d+/g); if (!digits || digits.length < 3) return null; const season = parseInt(digits[0], 10); const day = parseInt(digits[2], 10); return { currentDate, season, day }; }; const getSeasonCalculator = (cs) => { if (!cs) return () => 0; const baseSeason = cs.season; const baseDate = cs.currentDate; const dayOffset = cs.day; const seasonStart = new Date(baseDate); seasonStart.setDate(seasonStart.getDate() - (dayOffset)); return d => { if (!(d instanceof Date)) return 0; let s = baseSeason; let ref = seasonStart.getTime(); let diff = Math.floor((d.getTime() - ref) / 86400000); while (diff < 0) { s--; diff += 91; } while (diff >= 91) { s++; diff -= 91; } return s; }; }; const calculateHistoricalAge = ({ currentAge, currentSeason, targetSeason }) => { if (!currentAge) return null; const seasonDiff = currentSeason - targetSeason; return currentAge - seasonDiff; }; const getPlayerContainerNode = (n) => { let c = n.closest('.playerContainer'); if (!c) c = document.querySelector('.playerContainer'); return c; }; const hasVisibleSkills = (container) => container.querySelector('table.player_skills') !== null; const parsePlayerAge = (container) => { const strongEls = container.querySelectorAll('strong'); for (const el of strongEls) { const numberMatch = el.textContent.trim().match(/^(\d{1,2})$/); if (numberMatch) { const age = parseInt(numberMatch[1], 10); if (age >= 15 && age <= 45) return age; } } const allNums = container.textContent.match(/\b(\d{1,2})\b/g); if (allNums) { for (const numString of allNums) { const age = parseInt(numString, 10); if (age >= 15 && age <= 45) return age; } } return null; }; const parsePriceString = (priceStr) => { if (!priceStr || priceStr === 'N/A' || priceStr === '-') return { amount: 0, currency: '' }; const match = priceStr.match(/(\d[\d\s]+)(?:\s*)([A-Za-z$]+|点|MM|R\$)/); if (!match) return { amount: 0, currency: '' }; const rawAmount = match[1].replace(/\s+/g, ''); const amount = parseFloat(rawAmount); const currency = match[2]; return { amount, currency }; }; const convertPrice = (priceObj, targetCurrency) => { if (!priceObj.amount || !priceObj.currency || !CURRENCIES[priceObj.currency]) { return { amount: 0, currency: targetCurrency }; } const sourceRate = CURRENCIES[priceObj.currency]; const targetRate = CURRENCIES[targetCurrency]; if (!sourceRate || !targetRate) { return { amount: priceObj.amount, currency: priceObj.currency }; } const inSEK = priceObj.amount * sourceRate; const convertedAmount = inSEK / targetRate; return { amount: Math.round(convertedAmount), currency: targetCurrency }; }; const formatPrice = (priceObj) => { if (!priceObj.amount) return 'N/A'; const formatted = new Intl.NumberFormat().format(priceObj.amount); return `${formatted} ${priceObj.currency}`; }; const parseSeriesData = (txt) => { const m = txt.match(/var series = (\[.*?\]);/); return m ? JSON.parse(m[1]) : null; }; const sanitizeChipName = (name) => { if (!name) return ''; return name.replace(/<[^>]*>/g, '').replace(/"/g, '').replace(/ No\. \d+/, '').trim(); }; const extractChipsInfo = (series) => { const chips = []; if (!series) return chips; series.forEach(s => { s.data.forEach(pt => { if (pt.marker?.symbol.includes('training_camp_chip.png') && pt.name) { const chipName = sanitizeChipName(pt.name); const date = new Date(pt.x); chips.push({ name: chipName, date: date, dateString: date.toLocaleDateString() }); } }); }); return chips; }; const generateChipDisplayHTML = (chipName, iconOnly = false) => { const lowerCaseChipName = chipName.toLowerCase(); let canonicalName = chipName; let iconFile = ''; for (const knownChip in SPECIAL_CHIP_ICONS) { if (lowerCaseChipName.includes(knownChip)) { canonicalName = knownChip; iconFile = `${SPECIAL_CHIP_ICONS[knownChip]}.png`; break; } } if (!iconFile) { const cleanName = lowerCaseChipName.replace(/ chip$/, '').replace(/ package$/, '').replace(/\s+/g, '_'); iconFile = `${cleanName}.png`; } const iconUrl = `img/training/chip/${iconFile}`; const displayName = canonicalName.charAt(0).toUpperCase() + canonicalName.slice(1); if (iconOnly) { return `<img src="${iconUrl}" class="th-chip-icon-table" title="${displayName}">`; } return ` <span class="th-chip-display" title="${displayName}"> <img src="${iconUrl}" class="th-chip-icon"> <span>${displayName}</span> </span> `; }; const gatherCurrentSkills = (container) => { const rows = container.querySelectorAll('table.player_skills tr'); const out = {}; let i = 1; rows.forEach(r => { const valCell = r.querySelector('.skillval span'); if (!valCell) return; const name = SKILL_MAP[i.toString()]; if (name) { const v = parseInt(valCell.textContent.trim(), 10); out[name] = isNaN(v) ? 0 : v; } i++; }); return out; }; const getTotalBallsFromSkillMap = (map) => Object.values(map).reduce((a, b) => a + b, 0); const processTrainingHistory = (series, getSeasonFn) => { const bySeason = {}; const skillTotals = {}; const chips = extractChipsInfo(series); const chipsBySeason = {}; const skillMaxedSeason = {}; let total = 0; let earliest = 9999; chips.forEach(chip => { const season = getSeasonFn(chip.date); if (!chipsBySeason[season]) chipsBySeason[season] = []; chipsBySeason[season].push(chip); }); if (series) { series.forEach(s => { s.data.forEach((pt, i) => { if (pt.marker?.symbol.includes('gained_skill.png') && s.data[i + 1]) { const nextPt = s.data[i + 1]; const d = new Date(nextPt.x - 1000); const sea = getSeasonFn(d); if (!bySeason[sea]) bySeason[sea] = []; const sid = nextPt.y.toString(); const sk = SKILL_MAP[sid] || 'Unknown'; const isMaxed = nextPt.hasOwnProperty('name'); bySeason[sea].push({ dateString: d.toLocaleDateString(), skillName: sk, maxed: isMaxed }); if (isMaxed && sk !== 'Unknown' && !skillMaxedSeason[sk]) { skillMaxedSeason[sk] = sea; } if (!skillTotals[sk]) skillTotals[sk] = 0; skillTotals[sk]++; total++; if (sea < earliest) earliest = sea; } }); }); } return { bySeason, skillTotals, total, earliestSeason: earliest, chips, chipsBySeason, skillMaxedSeason }; }; const fillSeasonGains = (bySeason, earliestSeason, currentSeason, skillTotals) => { const out = {}; for (let s = earliestSeason; s <= currentSeason; s++) { out[s] = {}; ORDERED_SKILLS.forEach(sk => { out[s][sk] = 0; }); if (bySeason[s]) { bySeason[s].forEach(ev => { if (skillTotals[ev.skillName]) { if (!out[s][ev.skillName]) out[s][ev.skillName] = 0; out[s][ev.skillName]++; } }); } } return out; }; const buildSeasonCheckpointData = (earliestSeason, currentSeason, finalMap, seasonGains, currentAge) => { const out = []; const currentMap = {}; ORDERED_SKILLS.forEach(sk => { currentMap[sk] = finalMap[sk] || 0; }); out.push({ season: currentSeason, label: 'Current', distribution: { ...finalMap } }); for (let s = currentSeason; s >= earliestSeason; s--) { if (seasonGains[s]) { Object.keys(seasonGains[s]).forEach(k => { if (currentMap.hasOwnProperty(k)) { currentMap[k] -= seasonGains[s][k]; if (currentMap[k] < 0) currentMap[k] = 0; } }); } const age = calculateHistoricalAge({ currentAge, currentSeason, targetSeason: s }); const label = age !== null ? `${s} (${age})` : s.toString(); const snapshot = { ...currentMap }; out.unshift({ season: s, label, distribution: snapshot }); } return out; }; const makeSkillRows = (params) => { const { map, prevMap, arrivalMap, currentSeasonForState, isCurrentState, scoutData, skillMaxedSeason } = params; let comparisonHtml = ''; let arrivalGainHtml = ''; let totalIncreaseFromPrev = 0; let totalGainSinceArrival = 0; ORDERED_SKILLS.forEach((k, idx) => { let v = map[k] || 0; if (v < 0) v = 0; if (v > 10) v = 10; let changeHTML = ''; let gainSinceArrivalTextHTML = ''; let initialBallsVizHTML = ''; let gainedBallsVizHTML = ''; let potentialClass = ''; let potentialIcon = ''; let skillNameSpecificClass = ''; const maxedSeasonForSkill = skillMaxedSeason[k]; const isVisuallyMaxed = (v === 10) || (maxedSeasonForSkill && maxedSeasonForSkill < currentSeasonForState); const isMaxedClass = isVisuallyMaxed ? ' th-skill-maxed' : ''; if (scoutData) { if (scoutData.hp > 0 && (k === scoutData.firstHpSkill || k === scoutData.secondHpSkill)) { skillNameSpecificClass = ` th-skill-potential-hp${scoutData.hp}`; } else if (scoutData.lp > 0 && (k === scoutData.firstLpSkill || k === scoutData.secondLpSkill)) { skillNameSpecificClass = ` th-skill-potential-lp${scoutData.lp}`; } if (scoutData.hp > 0 && scoutData.hpPotentialIndices?.includes(idx)) { potentialClass = ` th-skill-potential-hp${scoutData.hp}`; potentialIcon = `<i class="fas fa-star th-potential-icon th-potential-icon-hp${scoutData.hp}"></i>`; } else if (scoutData.lp > 0 && scoutData.lpPotentialIndices?.includes(idx)) { potentialClass = ` th-skill-potential-lp${scoutData.lp}`; potentialIcon = `<i class="fas fa-star th-potential-icon th-potential-icon-lp${scoutData.lp}"></i>`; } } if (prevMap) { const prevVal = prevMap[k] || 0; const change = v - prevVal; if (change > 0) { changeHTML = `<span class="th-skill-increase">(+${change})</span>`; totalIncreaseFromPrev += change; } } const baseSkillRowStartHtml = ` <div class="th-state-skill${potentialClass}"> <div class="th-skill-name${skillNameSpecificClass}">${potentialIcon}<strong>${k}</strong></div>`; comparisonHtml += `${baseSkillRowStartHtml} <div class="th-skill-val"> <img src="nocache-922/img/soccer/wlevel_${v}.gif" alt=""> <span class="th-skill-value-text${isMaxedClass}">(${v})</span> </div> <div class="th-skill-change">${changeHTML}</div> </div>`; if (arrivalMap && isCurrentState) { const arrivalVal = arrivalMap[k] || 0; const gainSinceArrival = v - arrivalVal; initialBallsVizHTML = `<span class="th-initial-balls">${arrivalVal > 0 ? '●'.repeat(arrivalVal) : ''}</span>`; if (gainSinceArrival > 0) { gainSinceArrivalTextHTML = `<span class="th-gain-since-arrival">(+${gainSinceArrival})</span>`; totalGainSinceArrival += gainSinceArrival; gainedBallsVizHTML = `<span class="th-gained-balls">${'●'.repeat(gainSinceArrival)}</span>`; } else { gainSinceArrivalTextHTML = ''; gainedBallsVizHTML = ''; } arrivalGainHtml += `${baseSkillRowStartHtml} <div class="th-skill-val th-arrival-skill-val"> ${initialBallsVizHTML} ${gainedBallsVizHTML} </div> <div class="th-skill-change">${gainSinceArrivalTextHTML}</div> </div>`; } }); return { comparisonHtml, arrivalGainHtml, totalIncrease: totalIncreaseFromPrev, totalGainSinceArrival }; }; const createModal = (content, spin) => { const ov = document.createElement('div'); ov.className = 'th-overlay'; const mo = document.createElement('div'); mo.className = 'th-modal'; const bd = document.createElement('div'); bd.className = 'th-modal-content'; const sp = document.createElement('div'); sp.style.height = '60px'; sp.style.display = spin ? 'block' : 'none'; bd.appendChild(sp); if (content) bd.innerHTML += content; const cl = document.createElement('div'); cl.className = 'th-modal-close'; cl.innerHTML = '×'; cl.onclick = () => { Object.values(comparisonChartInstances).forEach(instance => instance.dispose()); comparisonChartInstances = {}; ov.remove(); } mo.appendChild(cl); mo.appendChild(bd); ov.appendChild(mo); document.body.appendChild(ov); ov.addEventListener('click', e => { if (e.target === ov) { Object.values(comparisonChartInstances).forEach(instance => instance.dispose()); comparisonChartInstances = {}; ov.remove(); } }); requestAnimationFrame(() => { ov.classList.add('show'); mo.classList.add('show'); }); let spinnerInstance = null; if (spin) { spinnerInstance = new Spinner({ color: '#5555aa', lines: 12 }); spinnerInstance.spin(sp); } return { modal: mo, spinnerEl: sp, spinnerInstance, overlay: ov }; }; const generateEvolHTML = (processedData, currentAge, currentSeason) => { const { bySeason, total, skillTotals, chips, chipsBySeason } = processedData; let html = ''; const getSeason = getSeasonCalculator({ currentDate: new Date(), season: currentSeason, day: 1 }); const sortedSeasons = Object.keys(bySeason) .map(x => parseInt(x, 10)) .sort((a, b) => a - b); sortedSeasons.forEach(se => { const items = bySeason[se]; const age = calculateHistoricalAge({ currentAge, currentSeason, targetSeason: se }); const label = age !== null ? `Season ${se} (Age ${age})` : `Season ${se}`; let seasonChipsHtml = ''; if (chipsBySeason[se] && chipsBySeason[se].length > 0) { const chipDisplays = chipsBySeason[se].map(c => generateChipDisplayHTML(c.name)).join(''); seasonChipsHtml = `<div class="th-chips-list">Chips: ${chipDisplays}</div>`; } html += `<div class="th-training-season"> <h3>${label} — ${items.length} Ball${items.length !== 1 ? 's' : ''} Earned</h3> ${seasonChipsHtml} <ul>`; items.forEach(it => { const maxedIndicator = it.maxed ? ' <span class="th-maxed-indicator">(Maxed)</span>' : ''; html += `<li><strong>${it.dateString}</strong> ${it.skillName}${maxedIndicator}</li>`; }); html += '</ul></div>'; }); html += `<hr><h3 class="th-training-final-summary">Total Balls Earned: ${total}</h3>`; const fs = Object.entries(skillTotals) .filter(([, count]) => count > 0) .sort(([, countA], [, countB]) => countB - countA) .map(([skill, count]) => `${skill} (${count})`) .join(', '); html += `<h3 class="th-training-skilltotals">${fs}</h3>`; const allChipsSorted = [...chips].sort((a, b) => a.date - b.date); if (allChipsSorted.length > 0) { html += `<h3 class="th-training-final-summary">All Applied Chips</h3><ul class="th-all-chips-list">`; allChipsSorted.forEach(chip => { const chipSeason = getSeason(chip.date); html += `<li>S${chipSeason}: ${generateChipDisplayHTML(chip.name)} (${chip.dateString})</li>`; }); html += `</ul>`; } return html; }; const buildStatesLayout = (processedData, finalMap, currentAge, currentSeason, scoutData, transferData) => { const { bySeason, skillTotals, earliestSeason, chipsBySeason, skillMaxedSeason } = processedData; const seasonGains = fillSeasonGains(bySeason, earliestSeason, currentSeason, skillTotals); const arr = buildSeasonCheckpointData(earliestSeason, currentSeason, finalMap, seasonGains, currentAge); const arrivalMap = arr.length > 0 ? arr[0].distribution : null; let paginatedHtml = '<div class="th-state-wrapper th-paginated-view">'; let allViewHtml = '<div class="th-state-wrapper th-all-view" style="display:none;">'; arr.forEach((o, index) => { const sum = getTotalBallsFromSkillMap(o.distribution); let headerText; const isCurrent = o.label === 'Current'; const seasonNumber = isCurrent ? currentSeason : parseInt(o.label.split(' ')[0], 10); let stateSkillsHtml = ''; const prevDistribution = index > 0 ? arr[index - 1].distribution : null; const useArrivalMapForComparison = isCurrent ? arrivalMap : null; const skillRowsResult = makeSkillRows({ map: o.distribution, prevMap: prevDistribution, arrivalMap: useArrivalMapForComparison, currentSeasonForState: seasonNumber, isCurrentState: isCurrent, scoutData: scoutData, skillMaxedSeason: skillMaxedSeason }); if (isCurrent) { headerText = `Current State - Season ${currentSeason}`; if (currentAge !== null) headerText += ` (Age ${currentAge})`; stateSkillsHtml = ` <div class="th-state-skills"> <h5>Changes vs Start of Season ${currentSeason}</h5> ${skillRowsResult.comparisonHtml} ${skillRowsResult.totalIncrease > 0 ? `<div class="th-skill-total-increase"><span>(+${skillRowsResult.totalIncrease} total this season)</span></div>` : ''} </div> <div class="th-state-skills th-arrival-gains"> <h5>Gains Since Arrival (Season ${arr[0]?.season || '?'})</h5> ${skillRowsResult.arrivalGainHtml} ${skillRowsResult.totalGainSinceArrival > 0 ? `<div class="th-skill-total-increase"><span>(+${skillRowsResult.totalGainSinceArrival} total since arrival)</span></div>` : ''} </div> `; } else { const [seasonStr, ageStr] = o.label.split(' '); const agePart = ageStr ? ageStr.replace(/[()]/g, '') : '?'; if (index === 0) { headerText = `Arrival at Club - Season ${seasonStr}`; if (agePart !== '?') headerText += ` (Age ${agePart})`; } else { headerText = `Start of Season ${seasonStr}`; if (agePart !== '?') headerText += ` (Age ${agePart})`; } stateSkillsHtml = ` <div class="th-state-skills"> ${index > 0 ? `<h5>Changes vs Start of Season ${arr[index - 1]?.season}</h5>` : ''} ${skillRowsResult.comparisonHtml} ${skillRowsResult.totalIncrease > 0 ? `<div class="th-skill-total-increase"><span>(+${skillRowsResult.totalIncrease} total vs prev)</span></div>` : ''} </div> `; } let chipInfo = ''; if (chipsBySeason[seasonNumber] && chipsBySeason[seasonNumber].length > 0 && !isCurrent) { const chipDisplays = chipsBySeason[seasonNumber].map(c => generateChipDisplayHTML(c.name)).join(''); chipInfo = `<div class="th-chips-list">Chips used during S${seasonNumber}: ${chipDisplays}</div>`; } let transferInfo = ''; if (transferData && transferData[seasonNumber] && transferData[seasonNumber].length > 0 && !isCurrent) { transferInfo = '<div class="th-transfer-info" data-season="' + seasonNumber + '">' + '<div class="th-transfer-header">Season ' + seasonNumber + ': ' + '<i class="fa fa-cog th-transfer-currency-icon" title="Change currency"></i>' + '<div class="th-transfer-currency-dropdown"><ul>'; Object.keys(CURRENCIES).forEach(curr => { transferInfo += `<li data-currency="${curr}" ${curr === preferredCurrency ? 'class="selected"' : ''}>${curr}</li>`; }); transferInfo += '</ul></div></div><ul>'; transferData[seasonNumber].forEach(t => { const priceObj = parsePriceString(t.price); const convertedPrice = convertPrice(priceObj, preferredCurrency); const displayPrice = formatPrice(convertedPrice); transferInfo += `<li data-original-price="${t.price}" data-price-amount="${priceObj.amount}" data-price-currency="${priceObj.currency}"> ${t.dateString}: ${t.fromTeamName} <i class="fa fa-arrow-right"></i> ${t.toTeamName} (${displayPrice}) </li>`; }); transferInfo += '</ul></div>'; } const colHtml = `<div class="th-state-col" data-page="${index}" data-season="${seasonNumber}"> <h4>${headerText}</h4> <div class="th-state-info">Total Balls: <strong>${sum}</strong></div> ${stateSkillsHtml} ${chipInfo} ${transferInfo} </div>`; paginatedHtml += colHtml; allViewHtml += colHtml; }); paginatedHtml += '</div>'; allViewHtml += '</div>'; let scoutHtml = ''; if (scoutData) { const { trainingSpeed, hp, lp, firstHpSkill, secondHpSkill, firstLpSkill, secondLpSkill } = scoutData; const speedClass = `th-speed-s${trainingSpeed}`; const hpClass = `th-hp-h${hp}`; const lpClass = `th-lp-l${lp}`; const speedText = trainingSpeed > 0 ? `S${trainingSpeed}` : 'N/A'; const hpText = hp > 0 ? `HP${hp}` : 'N/A'; const lpText = lp > 0 ? `LP${lp}` : 'N/A'; let hpSkillsText = ''; if (hp > 0 && firstHpSkill) { hpSkillsText += `<span class="th-potential-skill th-skill-potential-hp${hp}">${firstHpSkill}</span>`; if (secondHpSkill) hpSkillsText += `/<span class="th-potential-skill th-skill-potential-hp${hp}">${secondHpSkill}</span>`; } hpSkillsText = hpSkillsText ? ` ${hpSkillsText}` : ''; let lpSkillsText = ''; if (lp > 0 && firstLpSkill) { lpSkillsText += `<span class="th-potential-skill th-skill-potential-lp${lp}">${firstLpSkill}</span>`; if (secondLpSkill) lpSkillsText += `/<span class="th-potential-skill th-skill-potential-lp${lp}">${secondLpSkill}</span>`; } lpSkillsText = lpSkillsText ? ` ${lpSkillsText}` : ''; scoutHtml = ` <div class="th-scout-info"> <span class="${speedClass}">TrainingSpeed ${speedText}</span> | <span class="${hpClass}">${hpText}</span>${hpSkillsText} | <span class="${lpClass}">${lpText}</span>${lpSkillsText} </div>`; } return scoutHtml + paginatedHtml + allViewHtml; }; const generateCompareHTML = () => ` <div class="th-compare-controls"> <textarea id="th-compare-pids" placeholder="Enter player IDs separated by commas"></textarea> <button id="th-compare-btn" class="th-action-btn">Compare Players</button> </div> <div id="th-compare-results" class="th-compare-results-wrapper"> <div id="th-compare-spinner" style="display: none; height: 60px;"></div> <div id="th-compare-textual-results"></div> <div id="th-compare-graphical-results" class="th-compare-graphical-results"></div> <div id="th-compare-error-log"></div> </div> `; const generateTabsHTML = (name, evo, st) => ` <h2 class="th-title">${name}</h2> <div class="th-tabs"> <div class="th-tab-row"> <div class="th-tab-buttons"> <button class="th-tab-btn active" data-tab="states">Player Development</button> <button class="th-tab-btn" data-tab="evolution">Gains Log</button> <button class="th-tab-btn" data-tab="compare">Compare</button> </div> <div class="th-pagination-controls"> <button class="th-pagination-btn th-prev-btn" disabled>←</button> <span class="th-pagination-indicator">1 / 1</span> <button class="th-pagination-btn th-next-btn" disabled>→</button> <button class="th-pagination-toggle">Show All</button> </div> </div> <div class="th-tab-content" data-content="evolution">${evo}</div> <div class="th-tab-content" data-content="compare">${generateCompareHTML()}</div> <div class="th-tab-content active" data-content="states">${st}</div> </div>`; const detachPaginationEvents = (modal) => { const prevBtn = modal.querySelector('.th-prev-btn'); const nextBtn = modal.querySelector('.th-next-btn'); const toggleBtn = modal.querySelector('.th-pagination-toggle'); if (prevBtn) { const newPrevBtn = prevBtn.cloneNode(true); prevBtn.parentNode.replaceChild(newPrevBtn, prevBtn); } if (nextBtn) { const newNextBtn = nextBtn.cloneNode(true); nextBtn.parentNode.replaceChild(newNextBtn, nextBtn); } if (toggleBtn) { const newToggleBtn = toggleBtn.cloneNode(true); toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn); } }; const attachPaginationEvents = (modal, initialIndex) => { const statesContent = modal.querySelector('.th-tab-content[data-content="states"]'); if (!statesContent) return; const prevBtn = modal.querySelector('.th-prev-btn'); const nextBtn = modal.querySelector('.th-next-btn'); const paginationIndicator = modal.querySelector('.th-pagination-indicator'); const toggleBtn = modal.querySelector('.th-pagination-toggle'); const paginatedView = statesContent.querySelector('.th-paginated-view'); const allView = statesContent.querySelector('.th-all-view'); if (!paginatedView || !allView || !prevBtn || !nextBtn || !paginationIndicator || !toggleBtn) return; const stateColumns = Array.from(paginatedView.querySelectorAll('.th-state-col')); const totalPages = stateColumns.length; let currentIndex = initialIndex; const updatePaginationUI = () => { if (totalPages === 0) { prevBtn.disabled = true; nextBtn.disabled = true; paginationIndicator.textContent = '0 / 0'; toggleBtn.style.display = 'none'; return; } toggleBtn.style.display = ''; prevBtn.disabled = currentIndex === 0; nextBtn.disabled = currentIndex === totalPages - 1; paginationIndicator.textContent = `${currentIndex + 1} / ${totalPages}`; stateColumns.forEach((col, index) => { col.style.display = index === currentIndex ? '' : 'none'; }); }; prevBtn.addEventListener('click', () => { if (paginatedView.style.display === 'none') return; if (currentIndex > 0) { currentIndex--; updatePaginationUI(); } }); nextBtn.addEventListener('click', () => { if (paginatedView.style.display === 'none') return; if (currentIndex < totalPages - 1) { currentIndex++; updatePaginationUI(); } }); toggleBtn.addEventListener('click', () => { const isPaginated = paginatedView.style.display !== 'none'; paginatedView.style.display = isPaginated ? 'none' : ''; allView.style.display = isPaginated ? '' : 'none'; toggleBtn.textContent = isPaginated ? 'Show Paginated' : 'Show All'; prevBtn.disabled = !isPaginated || currentIndex === 0; nextBtn.disabled = !isPaginated || currentIndex === totalPages - 1; if (!isPaginated) { updatePaginationUI(); } }); updatePaginationUI(); }; const initPaginationState = (modal) => { const statesContent = modal.querySelector('.th-tab-content[data-content="states"]'); if (!statesContent) return; const paginatedView = statesContent.querySelector('.th-paginated-view'); const allView = statesContent.querySelector('.th-all-view'); const paginationToggle = modal.querySelector('.th-pagination-toggle'); const paginationControls = modal.querySelector('.th-pagination-controls'); if (!paginatedView || !allView || !paginationToggle || !paginationControls) { if (paginationControls) paginationControls.style.display = 'none'; return; } const stateColumns = paginatedView.querySelectorAll('.th-state-col'); if (!stateColumns.length) { if (paginationControls) paginationControls.style.display = 'none'; return; } else { paginationControls.style.display = ''; } paginatedView.style.display = ''; allView.style.display = 'none'; paginationToggle.textContent = 'Show All'; let initialIndex = 0; stateColumns.forEach((col, index) => { col.style.display = index === initialIndex ? '' : 'none'; }); const paginationIndicator = modal.querySelector('.th-pagination-indicator'); const prevBtn = modal.querySelector('.th-prev-btn'); const nextBtn = modal.querySelector('.th-next-btn'); if (paginationIndicator) { paginationIndicator.textContent = `${initialIndex + 1} / ${stateColumns.length}`; } if (prevBtn) prevBtn.disabled = initialIndex === 0; if (nextBtn) nextBtn.disabled = initialIndex >= stateColumns.length - 1; detachPaginationEvents(modal); attachPaginationEvents(modal, initialIndex); }; const updateTransferPrices = (modal, newCurrency) => { const transferInfoSections = modal.querySelectorAll('.th-transfer-info'); transferInfoSections.forEach(section => { const items = section.querySelectorAll('li'); items.forEach(item => { const originalAmount = parseFloat(item.getAttribute('data-price-amount')); const originalCurrency = item.getAttribute('data-price-currency'); if (originalAmount && originalCurrency) { const priceObj = { amount: originalAmount, currency: originalCurrency }; const convertedPrice = convertPrice(priceObj, newCurrency); const displayPrice = formatPrice(convertedPrice); const text = item.innerHTML; const pricePattern = /\([^)]*\)(?=[^(]*$)/; item.innerHTML = text.replace(pricePattern, `(${displayPrice})`); } }); }); }; const setUpCurrencyDropdowns = (modal) => { const icons = modal.querySelectorAll('.th-transfer-currency-icon'); icons.forEach(icon => { const newIcon = icon.cloneNode(true); icon.parentNode.replaceChild(newIcon, icon); }); modal.querySelectorAll('.th-transfer-currency-icon').forEach(icon => { icon.addEventListener('click', function(e) { e.stopPropagation(); e.preventDefault(); const dropdown = this.nextElementSibling; dropdown.classList.toggle('show'); if (dropdown.classList.contains('show')) { const rect = this.getBoundingClientRect(); dropdown.style.top = (rect.bottom - rect.top) + 5 + 'px'; dropdown.style.left = '0px'; } }); }); modal.querySelectorAll('.th-transfer-currency-dropdown li').forEach(item => { const newItem = item.cloneNode(true); item.parentNode.replaceChild(newItem, item); newItem.addEventListener('click', function(e) { e.stopPropagation(); const newCurrency = this.getAttribute('data-currency'); preferredCurrency = newCurrency; GM_setValue('PREFERRED_CURRENCY', newCurrency); modal.querySelectorAll('.th-transfer-currency-dropdown li').forEach(li => { li.classList.remove('selected'); }); modal.querySelectorAll(`.th-transfer-currency-dropdown li[data-currency="${newCurrency}"]`).forEach(li => { li.classList.add('selected'); }); updateTransferPrices(modal, newCurrency); this.closest('.th-transfer-currency-dropdown').classList.remove('show'); }); }); }; const attachTabEvents = (modal) => { const tbs = modal.querySelectorAll('.th-tab-btn'); const cs = modal.querySelectorAll('.th-tab-content'); const paginationControls = modal.querySelector('.th-pagination-controls'); const statesContent = modal.querySelector('.th-tab-content[data-content="states"]'); if (!statesContent || !paginationControls) return; const updatePaginationVisibility = () => { const activeTab = modal.querySelector('.th-tab-btn.active')?.getAttribute('data-tab'); const isPaginatedTab = activeTab === 'states'; paginationControls.style.display = isPaginatedTab ? 'flex' : 'none'; }; setUpCurrencyDropdowns(modal); attachCompareTabEvents(modal); document.addEventListener('click', function(e) { if (!e.target.closest('.th-transfer-currency-dropdown') && !e.target.classList.contains('th-transfer-currency-icon')) { document.querySelectorAll('.th-transfer-currency-dropdown.show').forEach(dropdown => { dropdown.classList.remove('show'); }); } }); updatePaginationVisibility(); tbs.forEach(btn => { btn.addEventListener('click', () => { tbs.forEach(x => x.classList.remove('active')); btn.classList.add('active'); const t = btn.getAttribute('data-tab'); cs.forEach(cc => { cc.classList.toggle('active', cc.getAttribute('data-content') === t); }); updatePaginationVisibility(); if (t === 'states') { initPaginationState(modal); } }); }); }; const fetchScoutData = async (pid) => { try { const response = await fetch(`https://www.managerzone.com/ajax.php?p=players&sub=scout_report&pid=${pid}&sport=soccer`); if (!response.ok) throw new Error(`HTTP error ${response.status}`); const text = await response.text(); const doc = new DOMParser().parseFromString(text, 'text/html'); const defaultResult = { trainingSpeed: 0, hp: 0, lp: 0, firstHpSkill: '', secondHpSkill: '', firstLpSkill: '', secondLpSkill: '', hpPotentialIndices: [], lpPotentialIndices: [] }; const paperContent = doc.querySelector('.paper-content'); if (!paperContent) return defaultResult; const dlElement = paperContent.querySelector('dl'); if (!dlElement) return defaultResult; const hpDd = dlElement.querySelector('dd i.fa-line-chart')?.closest('dd'); const lpDd = dlElement.querySelector('dd i.fa-exclamation-triangle')?.closest('dd'); const speedDd = dlElement.querySelector('dd i.fa-heartbeat')?.closest('dd'); const getSkillsAndIndices = (dd) => { const skills = []; const indices = []; if (!dd) return { skills, indices }; const listItems = dd.querySelectorAll('ul li'); listItems.forEach((li, index) => { const span = li.querySelector('.blurred span:last-child'); if (span) { const skillName = span.textContent.trim(); skills.push(skillName); if (li.querySelector('.stars i.fa-star.lit')) { indices.push(index); } } }); return { skills, indices }; }; const getStars = (dd) => dd ? dd.querySelectorAll('.stars i.fa-star.lit').length : 0; const { skills: hpSkillsNative, indices: hpIndices } = getSkillsAndIndices(hpDd); const { skills: lpSkillsNative, indices: lpIndices } = getSkillsAndIndices(lpDd); const hpStars = getStars(hpDd); const lpStars = getStars(lpDd); const speedStars = getStars(speedDd); const firstHpNative = hpSkillsNative[0] || ''; const secondHpNative = hpSkillsNative[1] || ''; const firstLpNative = lpSkillsNative[0] || ''; const secondLpNative = lpSkillsNative[1] || ''; return { hp: hpStars, lp: lpStars, trainingSpeed: speedStars, firstHpSkill: getEnglishSkillName(firstHpNative), secondHpSkill: getEnglishSkillName(secondHpNative), firstLpSkill: getEnglishSkillName(firstLpNative), secondLpSkill: getEnglishSkillName(secondLpNative), hpPotentialIndices: hpIndices, lpPotentialIndices: lpIndices }; } catch (error) { return null; } }; const fetchTransferData = async (pid, getSeasonFn) => { const url = `https://www.managerzone.com/?p=players&pid=${pid}`; try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP error ${response.status}`); const text = await response.text(); const doc = new DOMParser().parseFromString(text, 'text/html'); let transferTable = null; const potentialTables = doc.querySelectorAll('table.hitlist'); for (const table of potentialTables) { if (!table.classList.contains('hitlist-compact-list-included') && table.id !== 'suspensionsList') { transferTable = table; break; } } if (!transferTable) { return {}; } const transfersBySeason = {}; const rows = transferTable.querySelectorAll('tbody tr'); rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length < 5) return; const dateCell = cells[0]; const actionOrFromTeamCell = cells[1]; const toTeamCell = cells[3]; const priceCell = cells[4]; const dateStrRaw = dateCell?.textContent.trim(); const dateMatch = dateStrRaw.match(/(\d{2})-(\d{2})-(\d{4})|(\d{4})-(\d{2})-(\d{2})/); if (!dateMatch) return; let day, month, year; if (dateMatch[1]) { day = dateMatch[1]; month = dateMatch[2]; year = dateMatch[3]; } else { day = dateMatch[6]; month = dateMatch[5]; year = dateMatch[4]; } const transferDate = new Date(`${year}-${month}-${day}`); if (isNaN(transferDate)) return; const season = getSeasonFn(transferDate); const fromTeamLink = actionOrFromTeamCell.querySelector('a[href*="tid="]'); const toTeamLink = toTeamCell.querySelector('a[href*="tid="]'); let fromTeamName = 'Youth Academy'; if (fromTeamLink) { fromTeamName = fromTeamLink.textContent.trim(); } else { const fromText = actionOrFromTeamCell.textContent.trim(); if (fromText && fromText !== '-') { fromTeamName = fromText; } } const toTeamName = toTeamLink ? toTeamLink.textContent.trim() : toTeamCell.textContent.trim(); const priceDiv = priceCell?.querySelector('div[title]'); let price = priceDiv?.title || priceCell?.textContent.trim() || 'N/A'; if (price === '-') price = 'N/A'; if (!transfersBySeason[season]) { transfersBySeason[season] = []; } transfersBySeason[season].push({ date: transferDate, dateString: transferDate.toLocaleDateString(), fromTeamName: fromTeamName, toTeamName: toTeamName, price: price }); }); Object.values(transfersBySeason).forEach(seasonTransfers => { seasonTransfers.sort((a, b) => a.date - b.date); }); return transfersBySeason; } catch (_error) { return {}; } }; function decodeHtmlEntities(text) { if (!text) return ''; const textarea = document.createElement('textarea'); textarea.innerHTML = text; return textarea.value; } async function fetchCurrentSkillsViaAjax(pid) { return new Promise((resolve, reject) => { const url = `https://www.managerzone.com/ajax.php?p=transfer&sub=transfer-search&sport=soccer&issearch=true&u=${pid}&nationality=all_nationalities&deadline=0&category=&valuea=&valueb=&bida=&bidb=&agea=15&ageb=45&birth_season_low=0&birth_season_high=100&tot_low=0&tot_high=120&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`; fetch(url, { credentials: 'include' }) .then(response => { if (!response.ok) { throw new Error(`HTTP Error: ${response.status}`); } return response.json(); }) .then(data => { if (data && data.players) { try { const decodedHtml = decodeHtmlEntities(data.players); const parser = new DOMParser(); const ajaxDoc = parser.parseFromString(decodedHtml, 'text/html'); const skillsTable = ajaxDoc.querySelector('.player_skills'); if (skillsTable) { const skills = {}; const skillRows = skillsTable.querySelectorAll('tbody > tr'); skillRows.forEach((row, index) => { const skillId = (index + 1).toString(); const skillName = SKILL_MAP[skillId]; if (skillName) { const valueElem = row.querySelector('td.skillval > span'); if (valueElem) { const value = parseInt(valueElem.textContent.trim().replace(/[()]/g, ''), 10); if (!isNaN(value)) { skills[skillName] = value; } else { skills[skillName] = 0; } } else { skills[skillName] = 0; } } }); if (Object.keys(skills).length === ORDERED_SKILLS.length) { resolve(skills); } else { reject("Could not extract all expected skills from the AJAX response table."); } } else { reject("Skills table not found in AJAX response."); } } catch (e) { reject("Error parsing AJAX response: " + e.message); } } else { reject("No player data found in AJAX response."); } }) .catch(error => { reject("Error during fetch request: " + error.message); }); }); } const fetchCombinedPlayerData = async (pid, node, getSeasonFn, csi) => { const cont = getPlayerContainerNode(node); if (!cont) return; const curSeason = csi.season; const nmEl = cont.querySelector('.player_name'); const nm = nmEl ? nmEl.textContent.trim() : 'Unknown Player'; const { modal, spinnerEl, spinnerInstance } = createModal('', true); const currentAge = parsePlayerAge(cont); try { let currentSkillsMap = {}; const isSpecificPlayerPage = window.location.href.includes('/?p=players&pid='); if (isSpecificPlayerPage) { if (hasVisibleSkills(cont)) { currentSkillsMap = gatherCurrentSkills(cont); } else { try { currentSkillsMap = await fetchCurrentSkillsViaAjax(pid); } catch (fetchError) { throw new Error(`Could not retrieve current skills via AJAX. ${fetchError.message || fetchError}`); } } } else { currentSkillsMap = gatherCurrentSkills(cont); } if (!currentSkillsMap || Object.keys(currentSkillsMap).length === 0) { throw new Error("Failed to determine current skills for player."); } const [trainingResponse, scoutData, transferData] = await Promise.all([ fetch(`https://www.managerzone.com/ajax.php?p=trainingGraph&sub=getJsonTrainingHistory&sport=soccer&player_id=${pid}`).then(res => res.ok ? res.text() : Promise.reject(`HTTP error ${res.status} for training data`)), fetchScoutData(pid), fetchTransferData(pid, getSeasonFn) ]); if (spinnerInstance) spinnerInstance.stop(); spinnerEl.style.display = 'none'; const series = parseSeriesData(trainingResponse); const processedTrainingData = processTrainingHistory(series, getSeasonFn); const transferSeasons = Object.keys(transferData).map(s => parseInt(s)).filter(s => !isNaN(s)); let effectiveEarliestSeason = processedTrainingData.earliestSeason; if (transferSeasons.length > 0) { const earliestTransferSeason = Math.min(...transferSeasons); effectiveEarliestSeason = Math.max(1, Math.min(effectiveEarliestSeason, earliestTransferSeason)); } if (effectiveEarliestSeason === 9999) { if (transferSeasons.length > 0) { effectiveEarliestSeason = Math.max(1, Math.min(...transferSeasons)); } else { effectiveEarliestSeason = curSeason; } } processedTrainingData.earliestSeason = effectiveEarliestSeason; const evoHTML = generateEvolHTML(processedTrainingData, currentAge, curSeason); const stHTML = buildStatesLayout(processedTrainingData, currentSkillsMap, currentAge, curSeason, scoutData, transferData); const finalHTML = generateTabsHTML(nm, evoHTML, stHTML); modal.querySelector('.th-modal-content').innerHTML = finalHTML; setTimeout(() => { attachTabEvents(modal); initPaginationState(modal); }, 50); } catch (error) { if (spinnerInstance) spinnerInstance.stop(); if(spinnerEl) spinnerEl.style.display = 'none'; const contentDiv = modal.querySelector('.th-modal-content'); if (contentDiv) { contentDiv.innerHTML = `<div class="th-error-message"><p>Failed to process player data. (${error.message || 'Unknown error'})</p></div>`; } } }; async function fetchComparativePlayerData(pid, getSeasonFn, currentSeason) { try { const playerPageResponse = await fetch(`https://www.managerzone.com/?p=players&pid=${pid}`); if(!playerPageResponse.ok) throw new Error(`HTTP ${playerPageResponse.status} for player page`); const playerPageText = await playerPageResponse.text(); const doc = new DOMParser().parseFromString(playerPageText, 'text/html'); const nameEl = doc.querySelector('.player_name'); const name = nameEl ? nameEl.textContent.trim() : `Player ${pid}`; const age = parsePlayerAge(doc.body); if (!age) throw new Error('Could not parse age'); const trainingResponse = await fetch(`https://www.managerzone.com/ajax.php?p=trainingGraph&sub=getJsonTrainingHistory&sport=soccer&player_id=${pid}`); if(!trainingResponse.ok) throw new Error(`HTTP ${trainingResponse.status} for training data`); const trainingText = await trainingResponse.text(); const scoutData = await fetchScoutData(pid); if(!scoutData) throw new Error('Could not fetch scout data'); const series = parseSeriesData(trainingText); const { bySeason, chipsBySeason } = processTrainingHistory(series, getSeasonFn); const gainsByAge = {}; Object.keys(bySeason).forEach(seasonStr => { const season = parseInt(seasonStr, 10); const historicalAge = calculateHistoricalAge({ currentAge: age, currentSeason, targetSeason: season }); if (historicalAge) { if (!gainsByAge[historicalAge]) { gainsByAge[historicalAge] = { gains: 0, chips: [] }; } gainsByAge[historicalAge].gains += bySeason[season].length; if (chipsBySeason[season]) { const chipNames = chipsBySeason[season].map(c => c.name); gainsByAge[historicalAge].chips.push(...chipNames); } } }); return { pid, name, trainingSpeed: scoutData.trainingSpeed || 0, gainsByAge, totalGains: Object.values(gainsByAge).reduce((sum, ageData) => sum + ageData.gains, 0) }; } catch (error) { throw new Error(`Failed to fetch data for PID ${pid}: ${error.message}`); } } function buildComparisonTable(playersData, allAges) { let tableHtml = `<table class="th-compare-table"><thead><tr><th>Age</th>`; playersData.forEach(p => { tableHtml += `<th>${p.name}</th>`; }); tableHtml += `</tr><tr><td class="th-table-subheader"></td>`; playersData.forEach(p => { tableHtml += `<td class="th-table-subheader th-speed-s${p.trainingSpeed}">${p.trainingSpeed > 0 ? `S${p.trainingSpeed}` : ''}</td>`; }); tableHtml += `</tr></thead><tbody>`; allAges.forEach(age => { tableHtml += `<tr><td>${age}</td>`; playersData.forEach(p => { const ageData = p.gainsByAge[age]; const gains = ageData?.gains || 0; const chips = ageData?.chips || []; let cellContent = gains > 0 ? gains : '-'; if (chips.length > 0) { cellContent += ' ' + chips.map(chipName => generateChipDisplayHTML(chipName, true)).join(''); } tableHtml += `<td>${cellContent}</td>`; }); tableHtml += `</tr>`; }); tableHtml += `</tbody><tfoot><tr><td>Total Gains</td>`; playersData.forEach(p => { tableHtml += `<td>${p.totalGains}</td>`; }); tableHtml += `</tr></tfoot></table>`; return tableHtml; } function renderComparisonChart(chartContainerId, players, chartTitle, isCumulative, dataKey, xAxisLabels, labelFormatter) { const container = document.getElementById('th-compare-graphical-results'); if (comparisonChartInstances[chartContainerId]) { comparisonChartInstances[chartContainerId].dispose(); delete comparisonChartInstances[chartContainerId]; } const oldChartWrapper = container.querySelector(`.th-chart-wrapper[data-chart-id="${chartContainerId}"]`); if (oldChartWrapper) { oldChartWrapper.remove(); } if (xAxisLabels.length === 0) return; const chartWrapper = document.createElement('div'); chartWrapper.className = 'th-chart-wrapper'; chartWrapper.dataset.chartId = chartContainerId; chartWrapper.innerHTML = `${chartTitle ? `<h3>${chartTitle}</h3>` : ''}<div id="${chartContainerId}" style="height: 100%; width: 100%;"></div>`; container.appendChild(chartWrapper); const chartDom = document.getElementById(chartContainerId); if (!chartDom) return; const myChart = echarts.init(chartDom); const colors = ['#3e95cd', '#8e5ea2', '#3cba9f', '#e8c3b9', '#c45850', '#ff8c00', '#5f9ea0', '#6b8e23', '#4682b4', '#d2691e']; const seriesData = players.map(p => { let cumulativeValue = 0; const playerDataSet = p[dataKey] || {}; const data = xAxisLabels.map(label => { let value = (dataKey === 'gainsByAge') ? (playerDataSet[label]?.gains || 0) : (playerDataSet[label] || 0); if (isCumulative) { cumulativeValue += value; return cumulativeValue; } return value; }); return { name: `${p.name} (S${p.trainingSpeed})`, type: 'line', smooth: true, data: data, }; }); const option = { color: colors, tooltip: { trigger: 'axis' }, legend: { data: seriesData.map(s => s.name), type: 'scroll' }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', boundaryGap: false, data: labelFormatter(xAxisLabels) }, yAxis: { type: 'value' }, series: seriesData }; myChart.setOption(option); comparisonChartInstances[chartContainerId] = myChart; } const attachCompareTabEvents = (modal) => { const compareBtn = modal.querySelector('#th-compare-btn'); if (!compareBtn) return; const csi = getCurrentSeasonInfo(); if (!csi) return; const getSeasonFn = getSeasonCalculator(csi); compareBtn.addEventListener('click', async () => { const pidsInput = modal.querySelector('#th-compare-pids'); const textualResultsDiv = modal.querySelector('#th-compare-textual-results'); const graphicalResultsDiv = modal.querySelector('#th-compare-graphical-results'); const errorLogDiv = modal.querySelector('#th-compare-error-log'); const spinnerDiv = modal.querySelector('#th-compare-spinner'); textualResultsDiv.innerHTML = ''; graphicalResultsDiv.innerHTML = ''; errorLogDiv.innerHTML = ''; Object.values(comparisonChartInstances).forEach(instance => instance.dispose()); comparisonChartInstances = {}; const pids = [...new Set(pidsInput.value.split(',').map(id => id.trim()).filter(id => /^\d+$/.test(id)))]; if (pids.length === 0) { textualResultsDiv.innerHTML = '<div class="th-no-data-message"><p>Please enter at least one valid player ID.</p></div>'; return; } const spinner = new Spinner({ color: '#5555aa', lines: 12 }).spin(spinnerDiv); spinnerDiv.style.display = 'block'; const results = await Promise.allSettled(pids.map(pid => fetchComparativePlayerData(pid, getSeasonFn, csi.season))); spinner.stop(); spinnerDiv.style.display = 'none'; const successfulPlayers = []; const failedPids = []; results.forEach((result, index) => { if (result.status === 'fulfilled') { successfulPlayers.push(result.value); } else { failedPids.push({ pid: pids[index], reason: result.reason.message }); } }); if (failedPids.length > 0) { errorLogDiv.innerHTML = '<p>Could not fetch data for the following players:</p><ul>' + failedPids.map(f => `<li><strong>ID ${f.pid}:</strong> ${f.reason}</li>`).join('') + '</ul>'; } if (successfulPlayers.length > 0) { successfulPlayers.sort((a, b) => b.trainingSpeed - a.trainingSpeed); const allAges = [...new Set(successfulPlayers.flatMap(p => Object.keys(p.gainsByAge)))] .map(age => parseInt(age, 10)) .sort((a, b) => a - b); if (allAges.length > 0) { textualResultsDiv.innerHTML = buildComparisonTable(successfulPlayers, allAges); const ageLabelFormatter = ages => ages.map(age => `${age} yrs`); renderComparisonChart( 'all-players-age-chart', successfulPlayers, '', false, 'gainsByAge', allAges, ageLabelFormatter ); } else { textualResultsDiv.innerHTML = '<div class="th-no-data-message"><p>No training history found for the selected players.</p></div>'; graphicalResultsDiv.innerHTML = '<div class="th-no-data-message"><p>No training history found to generate charts.</p></div>'; } } else if (failedPids.length > 0) { textualResultsDiv.innerHTML = '<div class="th-error-message"><p>Failed to retrieve data for all specified players.</p></div>'; } }); }; const insertButtons = (getSeasonFn, csi) => { const containers = document.querySelectorAll('.playerContainer'); const isPlayerProfilePage = window.location.href.includes('/?p=players&pid='); containers.forEach(cc => { const targetElements = cc.querySelectorAll('.floatRight[id^="player_id_"]'); targetElements.forEach(ff => { const pidSpan = ff.querySelector('.player_id_span'); if (!pidSpan) return; const pid = pidSpan.textContent.trim(); if (!pid) return; const existingBtn = ff.querySelector('.th-btn'); if (existingBtn) return; let shouldInsert = false; const disabledGraphIcon = cc.querySelector('.training-graphs-icon.training-graphs-icon--disabled'); if (!disabledGraphIcon) { if (isPlayerProfilePage) { const trainingGraphIcon = document.querySelector( `span.player_icon_placeholder.training_graphs.soccer a[href*="p=training_graphs"][href*="pid=${pid}"]` ); if (trainingGraphIcon) { shouldInsert = true; } } else { if (hasVisibleSkills(cc)) { shouldInsert = true; } } } if (shouldInsert) { const b = document.createElement('button'); b.className = 'th-btn'; b.innerHTML = '<i class="fa fa-chart-line"></i>'; b.title = 'View Training History'; b.onclick = (e) => { e.preventDefault(); fetchCombinedPlayerData(pid, ff, getSeasonFn, csi); }; ff.appendChild(b); } }); }); }; const initTeamId = () => { const stored = GM_getValue('TEAM_ID'); if (stored) { myTeamId = stored; return; } const usernameEl = document.querySelector('#header-username'); if (!usernameEl) return; const username = usernameEl.textContent.trim(); if (!username) return; fetch(`https://www.managerzone.com/xml/manager_data.php?sport_id=1&username=${encodeURIComponent(username)}`) .then(response => { if (!response.ok) throw new Error(`HTTP error ${response.status}`); return response.text(); }) .then(text => { const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/xml'); const teamNodes = doc.querySelectorAll('Team[sport="soccer"]'); if (!teamNodes || !teamNodes.length) return; const tid = teamNodes[0].getAttribute('teamId'); if (tid) { GM_setValue('TEAM_ID', tid); myTeamId = tid; } }).catch((_err) => {}); }; const run = () => { initTeamId(); if (!canRunUserscript()) { return; } const csi = getCurrentSeasonInfo(); if (!csi) { return; } const getSeasonFn = getSeasonCalculator(csi); insertButtons(getSeasonFn, csi); const obs = new MutationObserver((mutations) => { let playerContainerChanged = false; for (const mutation of mutations) { if (mutation.type === 'childList') { if (mutation.target.matches && (mutation.target.matches('.playerContainer') || mutation.target.querySelector('.playerContainer'))) { playerContainerChanged = true; break; } for (const node of mutation.addedNodes) { if (node.nodeType === 1 && (node.matches('.playerContainer') || node.querySelector('.playerContainer'))) { playerContainerChanged = true; break; } } if (playerContainerChanged) break; } } if (playerContainerChanged) { insertButtons(getSeasonFn, csi); } }); obs.observe(document.body, { childList: true, subtree: true }); }; run(); })(); }).call(window); }); /* mzcopilot:module:[email protected] */ window.__mzcMega.run("transfer-export", "0.1.0", function () { (function () { (function () { 'use strict'; /** ========================== * CONFIG / LABELS / HELPERS * ========================== */ // Mapeamento de labels dos atributos → chave no CSV // Aceita variações em PT/EN/ES e ignora maiúsc/minúsc. const ATTR_LABELS = { // Físicos / mentais (ajuste se seu idioma estiver diferente) 'velocidade': 'Velocidade', 'speed': 'Velocidade', 'resist': 'Resistência', 'stamina': 'Resistência', 'intelig': 'Inteligência', 'intelligence': 'Inteligência', 'passe': 'Passe', 'passing': 'Passe', 'chute': 'Chute', 'shooting': 'Chute', 'cabece': 'Cabeceio', 'heading': 'Cabeceio', 'controle': 'Controle de Bola', 'ball control': 'Controle de Bola', 'marcação': 'Marcação', 'marking': 'Marcação', 'posicion': 'Posicionamento', 'positioning': 'Posicionamento', 'tackle': 'Desarme', 'desarme': 'Desarme', 'corrida com a bola': 'Corrida com Bola', 'dribbling': 'Corrida com Bola', 'finalização': 'Finalização', 'finishing': 'Finalização', 'cruzamento': 'Cruzamento', 'crossing': 'Cruzamento', 'experi': 'Experiência', 'experience': 'Experiência', // goleiro (se aparecer nos cards) 'goleiro': 'Goleiro', 'keeper': 'Goleiro' }; // Labels para estrelas (alto1, baixo2) e velocidade de treino const STAR_LABELS = { // “alto1” e “baixo2” são nomes de categorias no seu fluxo; aqui salvamos como colunas alto1: ['alto', 'high', 'alto1'], baixo2: ['baixo', 'low', 'baixo2'], treino: ['velocidade de treino', 'training speed', 'velocidad de entrenamiento'] }; // Funções auxiliares const $ = (sel, root = document) => root.querySelector(sel); const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel)); const normText = (t) => (t || '').replace(/\s+/g, ' ').trim(); const onlyDigits = (t) => (t || '').replace(/[^\d]/g, ''); const toNumber = (t) => { if (t == null) return null; const n = String(t).replace(/[^\d.,-]/g, '').replace(/\./g, '').replace(',', '.'); const v = parseFloat(n); return Number.isFinite(v) ? v : null; }; // CSV safe const csvCell = (v) => { if (v == null) return ''; const s = String(v); return /[;"\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; }; // Tenta ler número de "bolas" (total de pontos/níveis) exibidos no card function parseTotalBolas(container) { // Heurística 1: badges/bolinhas como <span class="ball ..."> repetidas const balls = $$('.ball', container); if (balls.length) return balls.length; // Heurística 2: texto tipo "Total de bolas: 123" const byText = $$('.player-attr, .small, .tiny, .text-muted', container) .map(el => normText(el.textContent)) .find(t => /total.*bolas|balls total|total.*pontos/i.test(t)); if (byText) { const n = toNumber(onlyDigits(byText)); if (n) return n; } return ''; } // Conta estrelas num bloco (checa <i class="fa-star">, <img alt="*">, etc.) function countStarsIn(node) { if (!node) return ''; const starLike = $$('i[class*="star"], svg[class*="star"], img[alt*="star"], img[src*="star"]', node); if (starLike.length) { // Se houver half-stars, ajuste aqui (ex.: class*="half"); por ora, contamos inteiras. return starLike.length; } // Fallback: procura “★” const txt = normText(node.textContent); const stars = (txt.match(/★/g) || []).length; return stars || ''; } // Encontra um bloco próximo cujo label contem determinado termo function findBlockByLabel(root, labelCandidates) { const nodes = $$('*', root).filter(el => { const t = normText(el.textContent).toLowerCase(); return labelCandidates.some(lab => t.includes(lab)); }); return nodes[0] || null; } // Lê “alto/baixo” determinante a partir de rótulos contendo (1) ou (2) function parseAltoBaixoDeterminate(attrList) { // attrList: array de {label, value, rawLabel} // Regra: se o label contém "1", determinante = 'alto1'; se contém "2", 'baixo2'. // Salva também o NOME do atributo que carregou o 1 ou 2. let det = ''; let atributo = ''; for (const row of attrList) { const raw = (row.rawLabel || '').toLowerCase(); if (/\b1\b|\(1\)/.test(raw)) { det = '1'; atributo = row.label; break; } if (/\b2\b|\(2\)/.test(raw)) { det = '2'; atributo = row.label; break; } } return { alto_baixo: det, atributo_determinante: atributo }; } // Extrai ID do jogador a partir de links function extractPlayerId(container) { const a = $('a[href*="player_id="]', container) || $('a[href*="players&"]', container); if (!a) return ''; const m = a.href.match(/player_id=(\d+)/); return m ? m[1] : ''; } // Extrai nome (a partir do link do jogador ou título do card) function extractName(container) { const a = $('a[href*="player_id="]', container); if (a && a.textContent) return normText(a.textContent); // fallback: o primeiro H-tag com texto const h = $('h3, h4, .player-name, .name', container); return h ? normText(h.textContent) : ''; } // Extrai idade e temporada de nascimento a partir de textos frequentes no card function extractAgeAndSeason(container) { const textNodes = $$('.small, .tiny, .text-muted, .player-meta, .details', container) .map(el => normText(el.textContent)); let idade = ''; let temporada = ''; for (const t of textNodes) { // Exemplos: "Idade: 23 (Nasc.: temp 95)", "Age: 23 (Born: season 95)" const mAge = t.match(/idade[:\s]*?(\d{1,2})|age[:\s]*?(\d{1,2})/i); if (mAge) idade = mAge[1] || mAge[2] || idade; const mSeason = t.match(/temp(?:orad[ao])?[:\s]*?(\d{2,3})|season[:\s]*?(\d{2,3})/i); if (mSeason) temporada = mSeason[1] || mSeason[2] || temporada; } return { idade, temporada_nascimento: temporada }; } // Extrai prazo final (deadline) e valor solicitado function extractDeadlineAndPrice(container) { const txts = $$('*', container).map(el => normText(el.textContent)); // Prazo final const tPrazo = txts.find(t => /prazo|deadline|fecha.*em|ends/i.test(t)) || ''; // ex.: "Prazo final: 2025-09-30 18:00", "Deadline: in 3h 14m", etc. let prazo_final = ''; const mDate = tPrazo.match(/\b(\d{4}-\d{2}-\d{2}.*?)$/) || tPrazo.match(/(\d{1,2}[:h]\d{2}.*)/); if (mDate) prazo_final = mDate[1]; // Valor solicitado const tPreco = txts.find(t => /valor|preço|asking|ask/i.test(t)) || ''; // números com milhar/ponto e decimal/vírgula const mMoney = tPreco.match(/([\d\.\,]+)/); const valor_solicitado = mMoney ? mMoney[1] : ''; return { prazo_final, valor_solicitado }; } // Extrai tabela/lista de atributos do card function extractAttributes(container) { const rows = []; // Pega linhas que parecem "Label: valor" ou estão lado a lado const possibleRows = $$('li, .row, .attr, .attribute, tr', container); for (const r of possibleRows) { const t = normText(r.textContent).toLowerCase(); // encontre um label conhecido const key = Object.keys(ATTR_LABELS).find(k => t.includes(k)); if (!key) continue; // label “cru” para detecção de (1)/(2) const rawLabelNode = $('*', r) || r; const rawLabel = normText(rawLabelNode.textContent); // tenta achar número do atributo no mesmo bloco let val = ''; // 1) números explícitos const m = rawLabel.match(/(\d{1,2})\s*$/) || normText(r.textContent).match(/(\d{1,2})\s*$/); if (m) val = m[1]; // 2) bolinhas (nível visual) if (!val) { const dots = $$('i[class*="dot"], span[class*="dot"], .ball', r).length; if (dots) val = String(dots); } rows.push({ label: ATTR_LABELS[key], value: val, rawLabel: rawLabel }); } // Consolida num objeto {Atributo: valor} const out = {}; for (const row of rows) { out[row.label] = row.value; } // Determina alto/baixo e atributo determinante const det = parseAltoBaixoDeterminate(rows); return { attrs: out, ...det }; } // Extrai estrelas para alto1, baixo2, velocidade de treino function extractStars(container) { const findAndCount = (labels) => { const node = findBlockByLabel(container, labels); return countStarsIn(node); }; return { estrelas_alto1: findAndCount(STAR_LABELS.alto1.map(s => s.toLowerCase())), estrelas_baixo2: findAndCount(STAR_LABELS.baixo2.map(s => s.toLowerCase())), estrelas_vel_treino: findAndCount(STAR_LABELS.treino.map(s => s.toLowerCase())) }; } // Seleciona “cards/linhas” de jogadores nos resultados function selectPlayerCards() { // Heurísticas comuns: // - tabela: linhas com link ?player_id=... // - cards: divs com link do jogador const byTable = $$('tr').filter(tr => $('a[href*="player_id="]', tr)); if (byTable.length) return byTable; const byCard = $$('div, li').filter(div => $('a[href*="player_id="]', div)); return byCard; } // Extrai 1 jogador a partir de um card/linha function scrapeOne(container) { const id = extractPlayerId(container); const nome = extractName(container); const { idade, temporada_nascimento } = extractAgeAndSeason(container); const total_bolas = parseTotalBolas(container); const { prazo_final, valor_solicitado } = extractDeadlineAndPrice(container); const { attrs, alto_baixo, atributo_determinante } = extractAttributes(container); const { estrelas_alto1, estrelas_baixo2, estrelas_vel_treino } = extractStars(container); return { ID: id, Nome: nome, Idade: idade, 'Temporada Nascimento': temporada_nascimento, 'Total de Bolas': total_bolas, 'Prazo Final': prazo_final, 'Valor Solicitado': valor_solicitado, // Atributos (ignorando Forma) Velocidade: attrs['Velocidade'] || '', 'Resistência': attrs['Resistência'] || '', Inteligência: attrs['Inteligência'] || '', Passe: attrs['Passe'] || '', Chute: attrs['Chute'] || '', Cabeceio: attrs['Cabeceio'] || '', 'Controle de Bola': attrs['Controle de Bola'] || '', Marcação: attrs['Marcação'] || '', Posicionamento: attrs['Posicionamento'] || '', Desarme: attrs['Desarme'] || '', 'Corrida com Bola': attrs['Corrida com Bola'] || '', Finalização: attrs['Finalização'] || '', Cruzamento: attrs['Cruzamento'] || '', Experiência: attrs['Experiência'] || '', // Estrelas 'Estrelas Alto1': estrelas_alto1, 'Estrelas Baixo2': estrelas_baixo2, 'Velocidade de Treinamento (★)': estrelas_vel_treino, // Determinante alto/baixo 'Alto/Baixo (1/2)': alto_baixo, 'Atributo Determinante': atributo_determinante }; } // Constrói CSV com separador ';' function toCSV(rows) { const headers = [ 'ID','Nome','Idade','Temporada Nascimento','Total de Bolas','Prazo Final','Valor Solicitado', 'Velocidade','Resistência','Inteligência','Passe','Chute','Cabeceio','Controle de Bola','Marcação', 'Posicionamento','Desarme','Corrida com Bola','Finalização','Cruzamento','Experiência', 'Estrelas Alto1','Estrelas Baixo2','Velocidade de Treinamento (★)', 'Alto/Baixo (1/2)','Atributo Determinante' ]; const lines = [ headers.map(csvCell).join(';'), ...rows.map(r => headers.map(h => csvCell(r[h])).join(';')) ]; return lines.join('\n'); } // Dispara download function download(name, text) { const blob = new Blob([text], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = name; document.body.appendChild(a); a.click(); setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 250); } /** ========================== * SCRAPER (PÁGINA ATUAL) * ========================== */ function scrapeCurrentPage() { const cards = selectPlayerCards(); const rows = []; for (const c of cards) { try { const row = scrapeOne(c); if (row.ID || row.Nome) rows.push(row); } catch (e) { // silencioso, segue o baile } } return rows; } /** ========================== * PAGINAÇÃO (TODAS AS PÁGINAS) * ========================== * * Estratégia: usamos ajaxSearchOffset se existir; caso não exista no escopo, * simulamos cliques nos links de paginação que tenham offset. */ async function scrapeAllPages() { const aggregate = []; const seen = new Set(); // Se a função global existir, iteramos offsets razoáveis (até 1000 registros) const ajaxFn = window.ajaxSearchOffset; if (typeof ajaxFn === 'function') { // Detecta quantos offsets existem na paginação atual (lê links) const getOffsetsFromDOM = () => $$('a[href*="offset="], a.page-link, .pagination a') .map(a => { const m = (a.getAttribute('onclick') || '').match(/ajaxSearchOffset\((\d+)\)/) || (a.href || '').match(/offset=(\d+)/); return m ? parseInt(m[1], 10) : null; }) .filter(v => Number.isInteger(v)); const offsets = Array.from(new Set([0, ...getOffsetsFromDOM()])).sort((a,b)=>a-b); for (const off of offsets) { await goOffset(off); await waitForResultsMutation(); const pageRows = scrapeCurrentPage(); for (const r of pageRows) { const key = r.ID || r.Nome + '|' + r['Prazo Final'] + '|' + r['Valor Solicitado']; if (!seen.has(key)) { seen.add(key); aggregate.push(r); } } } return aggregate; } // Fallback: sem ajaxSearchOffset — tentamos clicar nos links de paginação const pagerLinks = $$('a[href*="offset="], a.page-link, .pagination a'); if (!pagerLinks.length) { // Sem paginador → só atual return scrapeCurrentPage(); } // Primeira página aggregate.push(...scrapeCurrentPage()); // Demais páginas (abrindo em AJAX se for SPA, ou navegando) for (const a of pagerLinks) { if (a.click) a.click(); await waitForResultsMutation(); aggregate.push(...scrapeCurrentPage()); } return dedupe(aggregate); function dedupe(arr) { const out = []; const s = new Set(); for (const r of arr) { const key = r.ID || r.Nome + '|' + r['Prazo Final'] + '|' + r['Valor Solicitado']; if (!s.has(key)) { s.add(key); out.push(r); } } return out; } function waitForResultsMutation() { return new Promise(resolve => { const container = document.body; const obs = new MutationObserver((muts) => { // heurística: quando houver muitos links de players, deve ter carregado const count = $$('a[href*="player_id="]').length; if (count > 0) { obs.disconnect(); setTimeout(resolve, 100); // pequeno buffer } }); obs.observe(container, { childList: true, subtree: true }); // timeout de segurança setTimeout(() => { obs.disconnect(); resolve(); }, 4000); }); } async function goOffset(n) { try { const ret = ajaxFn(n); // Se ajaxFn não retornar Promise, aguarda um pequeno tempo if (ret && typeof ret.then === 'function') await ret; await new Promise(r => setTimeout(r, 200)); } catch (e) { await new Promise(r => setTimeout(r, 300)); } } } /** ========================== * UI (PAINEL FLUTUANTE) * ========================== */ function injectPanel() { if ($('#mz-export-panel')) return; const panel = document.createElement('div'); panel.id = 'mz-export-panel'; panel.innerHTML = ` <div style=" position: fixed; right: 14px; bottom: 14px; z-index: 99999; background: rgba(15, 17, 23, 0.9); color: #fff; padding: 12px 14px; border-radius: 12px; box-shadow: 0 6px 18px rgba(0,0,0,.25); font: 13px/1.3 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, sans-serif;"> <div style="font-weight:600;margin-bottom:8px;">MZ • Exportar Transferências</div> <div style="display:flex; gap:6px; flex-wrap:wrap; margin-bottom:8px;"> <button id="mz-btn-page" style="padding:6px 10px;border-radius:8px;border:none;cursor:pointer;">Coletar (página)</button> <button id="mz-btn-all" style="padding:6px 10px;border-radius:8px;border:none;cursor:pointer;">Coletar (todas páginas)</button> <button id="mz-btn-dl" style="padding:6px 10px;border-radius:8px;border:none;cursor:pointer;" disabled>Baixar CSV</button> </div> <div id="mz-status" style="opacity:.85">Aguardando…</div> </div> `; document.body.appendChild(panel); let buffer = []; $('#mz-btn-page').addEventListener('click', () => { try { const rows = scrapeCurrentPage(); buffer = rows; $('#mz-status').textContent = `Coletados (página): ${rows.length}`; $('#mz-btn-dl').disabled = rows.length === 0; } catch (e) { $('#mz-status').textContent = `Erro: ${e.message || e}`; } }); $('#mz-btn-all').addEventListener('click', async () => { $('#mz-status').textContent = 'Coletando todas as páginas…'; try { buffer = await scrapeAllPages(); $('#mz-status').textContent = `Coletados (todas páginas): ${buffer.length}`; $('#mz-btn-dl').disabled = buffer.length === 0; } catch (e) { $('#mz-status').textContent = `Erro: ${e.message || e}`; } }); $('#mz-btn-dl').addEventListener('click', () => { if (!buffer.length) return; const csv = toCSV(buffer); const stamp = new Date().toISOString().slice(0,19).replace(/[:T]/g,'-'); download(`mz_transferencias_${stamp}.csv`, csv); }); } /** ========================== * BOOT * ========================== */ function isTransferPage() { const href = location.href; return /[?&]p=transfer(&|$)/i.test(href) || /\/transfer/i.test(href); } function boot() { if (!isTransferPage()) return; injectPanel(); } // Espera DOM if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } })(); }).call(window); }); /* mzcopilot:module:[email protected] */ window.__mzcMega.run("opponent-tactics", "5.5.0", function () { (function () { (() => { 'use strict'; /** * @class OpponentTacticsPreview * @description A class responsible for all functionality related to previewing opponent tactics. */ class OpponentTacticsPreview { /** * @description A set of constant values used throughout the class. * @static * @readonly */ static CONSTANTS = { MATCH_TYPE_GROUPS: { 'All': [ { id: 'no_restriction', label: 'Senior' }, { id: 'u23', label: 'U23' }, { id: 'u21', label: 'U21' }, { id: 'u18', label: 'U18' } ], 'World League': [ { id: 'world_series', label: 'Senior WL' }, { id: 'u23_world_series', label: 'U23 WL' }, { id: 'u21_world_series', label: 'U21 WL' }, { id: 'u18_world_series', label: 'U18 WL' } ], 'Official League': [ { id: 'series', label: 'Senior League' }, { id: 'u23_series', label: 'U23 League' }, { id: 'u21_series', label: 'U21 League' }, { id: 'u18_series', label: 'U18 League' } ] }, URLS: { CLUBHOUSE: 'https://www.managerzone.com/?p=clubhouse', MATCH_LIST: 'https://www.managerzone.com/ajax.php?p=matches&sub=list&sport=soccer', MATCH_STATS: (matchId) => `https://www.managerzone.com/matchviewer/getMatchFiles.php?type=stats&mid=${matchId}&sport=soccer`, MATCH_RESULT: (matchId) => `https://www.managerzone.com/?p=match&sub=result&mid=${matchId}`, MATCH_CHECK: (matchId) => `https://www.managerzone.com/ajax.php?p=matchViewer&sub=check-match&type=2d&sport=soccer&mid=${matchId}`, PITCH_IMG: (matchId) => `https://www.managerzone.com/dynimg/pitch.php?match_id=${matchId}`, OFFICIAL_LEAGUE_SCHEDULE: (type, sid, tid) => `https://www.managerzone.com/ajax.php?p=league&type=${type}&sid=${sid}&tid=${tid}&sport=soccer&sub=schedule` }, STORAGE_KEYS: { MATCH_LIMIT: 'ylopp_match_limit', SAVED_TEAMS: 'ylopp_saved_teams', USER_TEAM_ID: 'ylopp_user_team_id', LEAGUE_CACHE_KEY: 'ylopp_league_data' }, DEFAULTS: { MATCH_LIMIT: 10, MAX_SAVED_TEAMS: 15, MAX_MATCH_LIMIT: 100 }, SELECTORS: { FIXTURES_LIST: '#fixtures-results-list-wrapper', STATS_XENTE: '#legendDiv', ELO_SCHEDULED: '#eloScheduledSelect', HOME_TEAM: '.home-team-column.flex-grow-1' }, CACHE_EXPIRATION_MS: 24 * 60 * 60 * 1000 }; /** * @constructor * @description Initializes the class properties. */ constructor() { this.myTeam = null; this.myTeamId = null; this.currentOpponentTid = ''; this.spinnerInstance = null; this.playstyleCache = {}; this.tooltipElement = null; this.tooltipHideTimeout = null; this.observer = new MutationObserver(() => { this.insertIconsAndListeners(); }); } /** * @description Retrieves the user's preferred match limit from storage. * @returns {number} The match limit. */ getMatchLimit = () => { return GM_getValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.MATCH_LIMIT, OpponentTacticsPreview.CONSTANTS.DEFAULTS.MATCH_LIMIT); }; /** * @description Sets the match limit in storage and provides user feedback. * @param {string} limit - The new limit value as a string. * @param {HTMLElement} confirmElem - The element to show the confirmation message. */ setMatchLimit = (limit, confirmElem) => { const numLimit = parseInt(limit, 10); if (!isNaN(numLimit) && numLimit > 0 && numLimit <= OpponentTacticsPreview.CONSTANTS.DEFAULTS.MAX_MATCH_LIMIT) { GM_setValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.MATCH_LIMIT, numLimit); if (confirmElem) { confirmElem.innerHTML = '<i class="fa fa-check"></i> Atualizado :)'; confirmElem.classList.add('visible'); setTimeout(() => { confirmElem.classList.remove('visible'); }, 2000); } } }; /** * @description Retrieves the list of recently saved teams. * @returns {Array<Object>} An array of team objects. */ getSavedTeams = () => { return GM_getValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.SAVED_TEAMS, []); }; /** * @description Saves a team ID and name to storage. * @param {string} teamId - The ID of the team. * @param {string} teamName - The name of the team. */ saveTeam = (teamId, teamName) => { if (!teamId || !teamName || teamName.startsWith('Team ')) { return; } let teams = this.getSavedTeams(); const existingIndex = teams.findIndex(team => team.id === teamId); if (existingIndex > -1) { teams.splice(existingIndex, 1); } teams.unshift({ id: teamId, name: teamName }); const trimmedTeams = teams.slice(0, OpponentTacticsPreview.CONSTANTS.DEFAULTS.MAX_SAVED_TEAMS); GM_setValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.SAVED_TEAMS, trimmedTeams); }; /** * @description Starts observing the DOM for mutations to re-insert icons. */ startObserving = () => { const fixturesList = document.querySelector(OpponentTacticsPreview.CONSTANTS.SELECTORS.FIXTURES_LIST); if (fixturesList) { this.observer.observe(fixturesList, { childList: true, subtree: true }); } }; /** * @description Displays a global loading spinner. */ showLoadingSpinner = () => { if (this.spinnerInstance) return; const spinnerContainer = document.createElement('div'); spinnerContainer.id = 'spinjs-overlay'; document.body.appendChild(spinnerContainer); this.spinnerInstance = new Spinner({ color: '#FFFFFF', lines: 12, top: '50%', left: '50%' }).spin(spinnerContainer); }; /** * @description Hides the global loading spinner. */ hideLoadingSpinner = () => { if (this.spinnerInstance) { this.spinnerInstance.stop(); this.spinnerInstance = null; } const spinnerContainer = document.getElementById('spinjs-overlay'); if (spinnerContainer) { spinnerContainer.remove(); } }; /** * @description Extracts a team name from an HTML document by finding the most frequent name. * @param {HTMLDocument} htmlDocument - The HTML document to parse. * @param {string} teamId - The ID of the team. * @returns {string|null} The team name or null if not found. */ extractTeamNameFromHtml = (htmlDocument, teamId) => { const nameCounts = new Map(); htmlDocument.querySelectorAll('.teams-wrapper a.clippable').forEach(link => { const linkUrl = new URL(link.href, location.href); const linkTid = linkUrl.searchParams.get('tid'); if (linkTid === teamId) { const fullName = link.querySelector('.full-name')?.textContent.trim(); if (fullName) { nameCounts.set(fullName, (nameCounts.get(fullName) || 0) + 1); } } }); if (nameCounts.size > 0) { const mostFrequentName = [...nameCounts.entries()].reduce((a, b) => b[1] > a[1] ? b : a)[0]; return mostFrequentName; } const boldTeamNameElement = htmlDocument.querySelector('.teams-wrapper a.clippable > strong > .full-name'); const boldName = boldTeamNameElement ? boldTeamNameElement.textContent.trim() : null; return boldName; }; /** * @description Fetches and processes the latest tactics for a given team. * @async * @param {string} teamId - The ID of the team. * @param {string} matchType - The type of match to search for. */ fetchLatestTactics = async (teamId, matchType) => { const modal = document.getElementById('interaction-modal'); if (modal) this.fadeOutAndRemove(modal); this.showLoadingSpinner(); try { const response = await fetch( OpponentTacticsPreview.CONSTANTS.URLS.MATCH_LIST, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, body: `type=played&hidescore=false&tid1=${teamId}&offset=&selectType=${matchType}&limit=max`, credentials: 'include' }); if (!response.ok) throw new Error(`Network response was not ok: ${response.statusText}`); const data = await response.json(); const parser = new DOMParser(); const htmlDocument = parser.parseFromString(data.list, 'text/html'); const actualTeamName = this.extractTeamNameFromHtml(htmlDocument, teamId); const finalTeamName = actualTeamName || `Team ${teamId}`; this.saveTeam(teamId, finalTeamName); this.currentOpponentTid = teamId; let matches = Array.from(htmlDocument.querySelectorAll('dl > dd.odd')) .filter(this.isRelevantMatch) .map(entry => this.parseMatchEntry(entry, finalTeamName)) .filter(Boolean); const officialLeagueTypes = ['series', 'u23_series', 'u21_series', 'u18_series']; if (officialLeagueTypes.includes(matchType)) { const leagueMatches = await this.fetchLeagueScheduleMatches(htmlDocument, finalTeamName, teamId); matches.push(...leagueMatches); } const uniqueMatches = Array.from(new Map(matches.map(m => [m.mid, m])).values()); this.processTacticsData(uniqueMatches, matchType, finalTeamName); } catch (error) { this.hideLoadingSpinner(); const message = document.createElement('div'); message.className = 'no-tactics-message'; message.textContent = 'Failed to fetch tactics data.'; const container = this.createTacticsContainer('Error', 'Data Fetch'); container.querySelector('.tactics-list').appendChild(message); document.body.appendChild(container); container.classList.add('fade-in'); } finally { this.hideLoadingSpinner(); } }; /** * @description Fetches official league schedule matches, utilizing a single, expiring cache. * @async * @param {HTMLDocument} initialHtmlDoc - The initial HTML document to find the league link. * @param {string} opponentName - The name of the opponent team. * @param {string} teamId - The ID of the team. * @returns {Promise<Array<Object>>} A promise that resolves to an array of parsed match data. */ fetchLeagueScheduleMatches = async (initialHtmlDoc, opponentName, teamId) => { try { const leagueLink = initialHtmlDoc.querySelector('.responsive-hide.match-reference-text-wrapper a'); if (!leagueLink) { return []; } const url = new URL(leagueLink.href, location.href); const sid = url.searchParams.get('sid'); const type = url.searchParams.get('type'); if (!sid || !type) { return []; } const cacheKey = OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.LEAGUE_CACHE_KEY; const cachedData = GM_getValue(cacheKey, {}); const cacheEntry = cachedData[sid]; if (cacheEntry && (Date.now() - cacheEntry.timestamp < OpponentTacticsPreview.CONSTANTS.CACHE_EXPIRATION_MS)) { const filteredMatches = cacheEntry.data.filter(m => m.homeTeamName === opponentName || m.awayTeamName === opponentName); return filteredMatches.map(m => this.parseMatchData(m, opponentName)); } const scheduleUrl = OpponentTacticsPreview.CONSTANTS.URLS.OFFICIAL_LEAGUE_SCHEDULE(type, sid, teamId); const response = await fetch(scheduleUrl); if (!response.ok) throw new Error(`Network response for league schedule was not ok: ${response.statusText}`); const text = await response.text(); const scheduleDoc = new DOMParser().parseFromString(text, 'text/html'); const allMatches = []; const rows = scheduleDoc.querySelectorAll('.hitlist.marker tr'); rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length !== 3) { return; } const scoreLink = cells[1].querySelector('a'); if (!scoreLink) { return; } const score = scoreLink?.textContent.trim(); if (!score || score.toLowerCase().includes('x')) { return; } const mid = new URL(scoreLink.href, location.href).searchParams.get('mid'); const homeTeamName = cells[0].textContent.trim(); const awayTeamName = cells[2].textContent.trim(); allMatches.push({ mid, homeTeamName, awayTeamName, score }); }); cachedData[sid] = { data: allMatches, timestamp: Date.now() }; GM_setValue(cacheKey, cachedData); return allMatches .filter(m => m.homeTeamName === opponentName || m.awayTeamName === opponentName) .map(m => this.parseMatchData(m, opponentName)); } catch (error) { return []; } }; /** * @description Checks if a match entry is relevant. * @param {HTMLElement} entry - The match entry element. * @returns {boolean} True if the match is relevant, otherwise false. */ isRelevantMatch = (entry) => { const wrapper = entry.querySelector('.responsive-hide.match-reference-text-wrapper'); return !wrapper || wrapper.querySelector('a') !== null; }; /** * @description Parses a match entry from an HTML element. * @param {HTMLElement} entry - The match entry element. * @param {string} opponentName - The name of the opponent team. * @returns {Object|null} The parsed match data or null. */ parseMatchEntry = (entry, opponentName) => { const link = entry.querySelector('a.score-shown'); if (!link) return null; const dl = link.closest('dl'); const score = link.textContent.trim(); const homeTeamName = dl.querySelector('.home-team-column .full-name')?.textContent.trim() || 'Home'; const awayTeamName = dl.querySelector('.away-team-column .full-name')?.textContent.trim() || 'Away'; const mid = new URL(link.href, location.href).searchParams.get('mid'); if (!mid) return null; return this.parseMatchData({ mid, homeTeamName, awayTeamName, score }, opponentName); }; /** * @description Parses match data and determines home/away goals. * @param {Object} matchData - The raw match data. * @param {string} opponentName - The name of the opponent team. * @returns {Object} The parsed match data with goal information. */ parseMatchData = (matchData, opponentName) => { let [homeGoals, awayGoals] = [0, 0]; if (matchData.score.includes('-')) { const parts = matchData.score.split('-').map(x => parseInt(x.trim(), 10)); if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) { [homeGoals, awayGoals] = parts; } } const opponentIsHome = (matchData.homeTeamName === opponentName); return { ...matchData, homeGoals, awayGoals, opponentIsHome }; }; /** * @description Creates and displays the tactics container with match data. * @param {Array<Object>} matches - An array of match objects. * @param {string} matchType - The type of match. * @param {string} opponentName - The name of the opponent team. */ processTacticsData = (matches, matchType, opponentName) => { const container = this.createTacticsContainer(matchType, opponentName); document.body.appendChild(container); const listWrapper = container.querySelector('.tactics-list'); const limit = this.getMatchLimit(); const limitedMatches = matches.slice(0, limit); if (limitedMatches.length === 0) { const message = document.createElement('div'); message.className = 'no-tactics-message'; message.textContent = 'No recent valid tactics found for this team and category.'; listWrapper.appendChild(message); container.classList.add('fade-in'); return; } limitedMatches.forEach(match => { const tacticUrl = OpponentTacticsPreview.CONSTANTS.URLS.PITCH_IMG(match.mid); const resultUrl = OpponentTacticsPreview.CONSTANTS.URLS.MATCH_RESULT(match.mid); const canvas = this.createCanvasWithReplacedColors(tacticUrl, match.opponentIsHome); const item = document.createElement('div'); item.className = 'tactic-item'; const opponentGoals = match.opponentIsHome ? match.homeGoals : match.awayGoals; const otherGoals = match.opponentIsHome ? match.awayGoals : match.homeGoals; if (opponentGoals > otherGoals) item.classList.add('tactic-win'); else if (opponentGoals < otherGoals) item.classList.add('tactic-loss'); else item.classList.add('tactic-draw'); const statusIndicator = document.createElement('div'); statusIndicator.className = 'playstyle-status-indicator'; item.appendChild(statusIndicator); const linkA = document.createElement('a'); linkA.href = resultUrl; linkA.target = '_blank'; linkA.className = 'tactic-link'; linkA.appendChild(canvas); const scoreP = document.createElement('p'); scoreP.textContent = `${match.homeTeamName} ${match.score} ${match.awayTeamName}`; linkA.appendChild(scoreP); item.appendChild(linkA); this.addPlaystyleHover(match.mid, this.currentOpponentTid, item); listWrapper.appendChild(item); }); container.classList.add('fade-in'); }; /** * @description Shows the interaction modal for selecting tactics options. * @param {string} teamId - The ID of the team. * @param {HTMLElement} sourceElement - The element that triggered the modal. */ showInteractionModal = (teamId, sourceElement) => { const existingModal = document.getElementById('interaction-modal'); if (existingModal) this.fadeOutAndRemove(existingModal); const modal = document.createElement('div'); modal.id = 'interaction-modal'; modal.classList.add('fade-in'); const header = document.createElement('div'); header.className = 'interaction-modal-header'; const title = document.createElement('span'); header.appendChild(title); const controlsWrapper = document.createElement('div'); controlsWrapper.style.display = 'flex'; controlsWrapper.style.alignItems = 'center'; controlsWrapper.style.gap = '10px'; const settingsIcon = document.createElement('span'); settingsIcon.className = 'settings-icon'; settingsIcon.innerHTML = '⚙'; controlsWrapper.appendChild(settingsIcon); const closeIcon = document.createElement('i'); closeIcon.className = 'fa fa-times ylotp-close-icon'; closeIcon.onclick = () => this.fadeOutAndRemove(modal); controlsWrapper.appendChild(closeIcon); header.appendChild(controlsWrapper); modal.appendChild(header); const teamInputSection = this.createTeamInputSection(modal, teamId); this.createTabbedButtons(modal, teamInputSection.teamIdInput); const settingsPanel = this.createSettingsPanel(modal); settingsIcon.onclick = () => { settingsPanel.style.display = settingsPanel.style.display === 'block' ? 'none' : 'block'; }; document.body.appendChild(modal); const rect = sourceElement.getBoundingClientRect(); modal.style.position = 'absolute'; const modalRect = modal.getBoundingClientRect(); const left = Math.min( window.scrollX + rect.left, window.scrollX + window.innerWidth - modalRect.width - 10 ); modal.style.left = `${Math.max(window.scrollX + 10, left)}px`; const initialTop = window.scrollY + rect.bottom + 5; const bottomAlignedTop = window.scrollY + window.innerHeight - modalRect.height - 10; modal.style.top = `${Math.max(window.scrollY + 10, Math.min(initialTop, bottomAlignedTop))}px`; }; /** * @description Creates the team ID input section for the modal. * @param {HTMLElement} container - The modal container. * @param {string} initialTeamId - The initial team ID to populate the input. * @returns {Object} An object containing the team ID input and recents dropdown. */ createTeamInputSection = (container, initialTeamId) => { const section = document.createElement('div'); section.className = 'interaction-section team-input-section'; const label = document.createElement('label'); label.textContent = 'Team ID:'; label.htmlFor = 'team-id-input'; section.appendChild(label); const teamIdInput = document.createElement('input'); teamIdInput.type = 'text'; teamIdInput.id = 'team-id-input'; teamIdInput.value = initialTeamId; section.appendChild(teamIdInput); const select = this.createRecentsDropdown(teamIdInput); section.appendChild(select); container.appendChild(section); return { teamIdInput, recentsSelect: select }; }; /** * @description Creates the "Recent Teams" dropdown for the modal. * @param {HTMLElement} teamIdInput - The team ID input element. * @returns {HTMLSelectElement} The created select element. */ createRecentsDropdown = (teamIdInput) => { const select = document.createElement('select'); select.className = 'recents-select'; const defaultOption = document.createElement('option'); defaultOption.textContent = 'Recent Teams'; defaultOption.value = ''; select.appendChild(defaultOption); this.getSavedTeams().forEach(team => { const option = document.createElement('option'); option.value = team.id; option.textContent = `${team.name} (${team.id})`; select.appendChild(option); }); select.onchange = () => { if (select.value) { teamIdInput.value = select.value; } }; return select; }; /** * @description Creates the tabbed buttons for match type selection. * @param {HTMLElement} container - The modal container. * @param {HTMLInputElement} teamIdInput - The team ID input element. */ createTabbedButtons = (container, teamIdInput) => { const tabContainer = document.createElement('div'); tabContainer.className = 'tab-container'; const tabHeaders = document.createElement('div'); tabHeaders.className = 'tab-headers'; const tabContents = document.createElement('div'); tabContents.className = 'tab-contents'; Object.entries(OpponentTacticsPreview.CONSTANTS.MATCH_TYPE_GROUPS).forEach(([groupName, types], index) => { const header = document.createElement('button'); header.className = 'tab-header'; header.textContent = groupName; const content = document.createElement('div'); content.className = 'tab-content'; types.forEach(type => { const button = document.createElement('button'); button.textContent = type.label; button.onclick = () => { const teamId = teamIdInput.value.trim(); if (!teamId || isNaN(parseInt(teamId, 10))) { const error = document.createElement('div'); error.className = 'error-message'; error.textContent = 'Please enter a valid Team ID.'; const existingError = tabContainer.querySelector('.error-message'); if (existingError) existingError.remove(); tabContainer.insertBefore(error, tabContents); return; } this.fetchLatestTactics(teamId, type.id); }; content.appendChild(button); }); header.onclick = () => { tabContainer.querySelectorAll('.tab-header').forEach(h => h.classList.remove('active')); tabContainer.querySelectorAll('.tab-content').forEach(c => c.style.display = 'none'); header.classList.add('active'); content.style.display = 'flex'; }; tabHeaders.appendChild(header); tabContents.appendChild(content); if (index === 0) { header.classList.add('active'); content.style.display = 'flex'; } else { content.style.display = 'none'; } }); tabContainer.appendChild(tabHeaders); tabContainer.appendChild(tabContents); container.appendChild(tabContainer); }; /** * @description Creates the settings panel for the modal. * @param {HTMLElement} modalContainer - The modal container. * @returns {HTMLElement} The created settings panel. */ createSettingsPanel = (modalContainer) => { const panel = document.createElement('div'); panel.className = 'settings-panel'; panel.style.display = 'none'; const limitLabel = document.createElement('label'); limitLabel.textContent = `MatchLimit (1-${OpponentTacticsPreview.CONSTANTS.DEFAULTS.MAX_MATCH_LIMIT}):`; panel.appendChild(limitLabel); const inputWrapper = document.createElement('div'); inputWrapper.style.display = 'flex'; inputWrapper.style.alignItems = 'center'; const limitInput = document.createElement('input'); limitInput.type = 'text'; limitInput.inputMode = 'numeric'; limitInput.pattern = '[0-9]*'; limitInput.value = this.getMatchLimit(); const confirmationSpan = document.createElement('span'); confirmationSpan.className = 'save-confirmation'; limitInput.oninput = () => { limitInput.value = limitInput.value.replace(/\D/g, ''); confirmationSpan.classList.remove('visible'); }; limitInput.onchange = () => this.setMatchLimit(limitInput.value, confirmationSpan); inputWrapper.appendChild(limitInput); inputWrapper.appendChild(confirmationSpan); panel.appendChild(inputWrapper); const note = document.createElement('small'); note.textContent = 'Note: the actual number of matches found may be restricted by MZ\'s own limits.'; panel.appendChild(note); modalContainer.appendChild(panel); return panel; }; /** * @description Creates the container for displaying tactics. * @param {string} matchType - The type of match. * @param {string} opponent - The opponent's name. * @returns {HTMLElement} The created tactics container. */ createTacticsContainer = (matchType, opponent) => { const existingContainer = document.getElementById('tactics-container'); if (existingContainer) { this.fadeOutAndRemove(existingContainer); } const container = document.createElement('div'); container.id = 'tactics-container'; container.className = 'tactics-container'; const header = document.createElement('div'); header.className = 'tactics-header'; const title = document.createElement('div'); title.className = 'match-info-text'; let matchTypeLabel = matchType; for (const group in OpponentTacticsPreview.CONSTANTS.MATCH_TYPE_GROUPS) { const found = OpponentTacticsPreview.CONSTANTS.MATCH_TYPE_GROUPS[group].find(t => t.id === matchType); if (found) { matchTypeLabel = found.label; break; } } title.innerHTML = `<div class="title-main">${opponent} (${matchTypeLabel})</div>`; header.appendChild(title); const closeButton = document.createElement('button'); closeButton.className = 'close-button'; closeButton.textContent = '×'; closeButton.onclick = () => this.fadeOutAndRemove(container); header.appendChild(closeButton); container.appendChild(header); const listWrapper = document.createElement('div'); listWrapper.className = 'tactics-list'; container.appendChild(listWrapper); return container; }; /** * @description Fades out and removes an element from the DOM. * @param {HTMLElement} el - The element to fade out. */ fadeOutAndRemove = (el) => { if (!el) return; el.classList.remove('fade-in'); el.classList.add('fade-out'); setTimeout(() => el.remove(), 200); }; /** * @description Identifies the user's team name from the current page. * @returns {string|null} The user's team name or null. */ identifyUserTeamName = () => { const ddRows = document.querySelectorAll('#fixtures-results-list > dd.odd'); if (ddRows.length === 0) return null; const countMap = new Map(); ddRows.forEach(dd => { const homeName = dd.querySelector('.home-team-column .full-name')?.textContent.trim(); const awayName = dd.querySelector('.away-team-column .full-name')?.textContent.trim(); if (homeName) countMap.set(homeName, (countMap.get(homeName) || 0) + 1); if (awayName) countMap.set(awayName, (countMap.get(awayName) || 0) + 1); }); if (countMap.size === 0) return null; const mostFrequentName = [...countMap.entries()].reduce((a, b) => b[1] > a[1] ? b : a)[0]; return mostFrequentName; }; /** * @description Inserts the magnifying glass icons and sets up event listeners. */ insertIconsAndListeners = () => { if (!this.myTeam) this.myTeam = this.identifyUserTeamName(); if (!this.myTeam) { return; } document.querySelectorAll('dd.odd').forEach(dd => { const selectWrapper = dd.querySelector('.set-default-wrapper'); if (selectWrapper && !selectWrapper.querySelector('.magnifier-icon')) { const homeTeamName = dd.querySelector('.home-team-column .full-name')?.textContent.trim(); const awayTeamName = dd.querySelector('.away-team-column .full-name')?.textContent.trim(); const homeTeamLink = dd.querySelector('.home-team-column a.clippable'); const awayTeamLink = dd.querySelector('.away-team-column a.clippable'); let opponentName = null; let opponentTid = null; if (homeTeamName === this.myTeam && awayTeamName && awayTeamLink) { opponentName = awayTeamName; if (awayTeamLink.href) opponentTid = new URL(awayTeamLink.href, location.href).searchParams.get('tid'); } else if (awayTeamName === this.myTeam && homeTeamName && homeTeamLink) { opponentName = homeTeamName; if (homeTeamLink.href) opponentTid = new URL(homeTeamLink.href, location.href).searchParams.get('tid'); } if (opponentName && opponentTid && (opponentTid !== this.myTeamId)) { const iconWrapper = document.createElement('span'); iconWrapper.className = 'magnifier-icon'; iconWrapper.dataset.tid = opponentTid; iconWrapper.dataset.opponent = opponentName; iconWrapper.title = 'Check opponent latest tactics'; iconWrapper.textContent = '🔍'; selectWrapper.querySelector('select')?.insertAdjacentElement('afterend', iconWrapper); } } }); }; /** * @description Creates a canvas with a pitch image and replaces colors. * @param {string} imageUrl - The URL of the image. * @param {boolean} opponentIsHome - True if the opponent is the home team. * @returns {HTMLCanvasElement} The created canvas element. */ createCanvasWithReplacedColors = (imageUrl, opponentIsHome) => { const canvas = document.createElement('canvas'); canvas.width = 150; canvas.height = 200; const context = canvas.getContext('2d'); const image = new Image(); image.crossOrigin = 'Anonymous'; image.onload = () => { if (opponentIsHome) { context.translate(canvas.width / 2, canvas.height / 2); context.rotate(Math.PI); context.translate(-canvas.width / 2, -canvas.height / 2); } context.drawImage(image, 0, 0, canvas.width, canvas.height); const imageData = context.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; const darkGreen = { r: 0, g: 100, b: 0 }; for (let i = 0; i < data.length; i += 4) { const r = data[i], g = data[i + 1], b = data[i + 2]; const isBlack = r < 30 && g < 30 && b < 30; const isYellow = r > 200 && g > 200 && b < 100; if (opponentIsHome) { if (isYellow) { data[i] = 0; data[i + 1] = 0; data[i + 2] = 0; } else if (isBlack) { data[i] = darkGreen.r; data[i + 1] = darkGreen.g; data[i + 2] = darkGreen.b; } } else { if (isBlack) { data[i] = 0; data[i + 1] = 0; data[i + 2] = 0; } else if (isYellow) { data[i] = darkGreen.r; data[i + 1] = darkGreen.g; data[i + 2] = darkGreen.b; } } } const tempData = new Uint8ClampedArray(data); for (let y = 1; y < canvas.height - 1; y++) { for (let x = 1; x < canvas.width - 1; x++) { const i = (y * canvas.width + x) * 4; if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0) { for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { if (dx === 0 && dy === 0) continue; const ni = ((y + dy) * canvas.width + (x + dx)) * 4; if (!(data[ni] === 0 && data[ni + 1] === 0 && data[ni + 2] === 0)) { tempData[i] = 255; tempData[i + 1] = 255; tempData[i + 2] = 255; } } } } } } context.putImageData(new ImageData(tempData, canvas.width, canvas.height), 0, 0); }; image.src = imageUrl; return canvas; }; /** * @description Ensures a match file is ready for fetching stats. * @async * @param {string} matchId - The ID of the match. * @param {number} [attempt=1] - The current attempt number. * @returns {Promise<void>} A promise that resolves when the file is ready. */ ensureMatchFileIsReady = (matchId, attempt = 1) => { const maxAttempts = 5; return new Promise(async (resolve, reject) => { if (attempt > maxAttempts) { const error = new Error(`File preparation failed after ${maxAttempts} attempts.`); return reject(error); } try { const response = await fetch(OpponentTacticsPreview.CONSTANTS.URLS.MATCH_CHECK(matchId)); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data = await response.json(); switch (data.response) { case 'ok': resolve(); break; case 'queued': setTimeout(() => this.ensureMatchFileIsReady(matchId, attempt + 1).then(resolve).catch(reject), 3000); break; default: reject(new Error(`Match file unavailable: ${data.response}.`)); break; } } catch (error) { reject(error); } }); }; _parseTimeToSeconds = (timeString) => { if (!timeString || !timeString.includes(':')) return 0; const parts = timeString.split(':'); const minutes = parseInt(parts[0], 10); const seconds = parseFloat(parts[1]); return isNaN(minutes) || isNaN(seconds) ? 0 : (minutes * 60) + seconds; }; _getSortedGoalEvents = (xml) => { const events = []; xml.querySelectorAll('Player > Goal').forEach(node => { const timeAttr = node.getAttribute('clock') || node.getAttribute('time'); if (!timeAttr) return; const timeInSeconds = this._parseTimeToSeconds(timeAttr); const teamId = node.getAttribute('team'); if (teamId) events.push({ time: timeInSeconds, type: 'Goal', teamId }); }); return events.sort((a, b) => a.time - b.time); }; _calculateScoreAtTime = (sortedGoalEvents, targetTime, homeTeamId, awayTeamId) => { let homeScore = 0; let awayScore = 0; for (const event of sortedGoalEvents) { if (event.time >= targetTime) break; if (event.teamId === homeTeamId) homeScore++; if (event.teamId === awayTeamId) awayScore++; } return { homeScore, awayScore }; }; /** * @description Fetches and formats playstyle changes from match stats. * @async * @param {string} mid - The ID of the match. * @param {string} opponentTid - The ID of the opponent team. * @returns {Promise<string>} A promise that resolves to an HTML string with playstyle data. */ fetchPlaystyleChanges = async (mid, opponentTid) => { await this.ensureMatchFileIsReady(mid); const response = await fetch(OpponentTacticsPreview.CONSTANTS.URLS.MATCH_STATS(mid)); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const text = await response.text(); const xml = new DOMParser().parseFromString(text, 'text/xml'); const opponentTeamNode = xml.querySelector(`Team[id="${opponentTid}"]`); if (!opponentTeamNode) return 'Opponent data not found.'; const isVisiting = opponentTeamNode.getAttribute('visiting') === '1'; const homeTeamId = xml.querySelector('Team[visiting="0"]')?.getAttribute('id'); const awayTeamId = xml.querySelector('Team[visiting="1"]')?.getAttribute('id'); const tooltipLines = []; tooltipLines.push(`Tactic: ${opponentTeamNode.getAttribute('tactic') || 'N/A'}`); tooltipLines.push(`Playstyle: ${opponentTeamNode.getAttribute('playstyle') || 'N/A'}`); tooltipLines.push(`Aggression: ${opponentTeamNode.getAttribute('aggression') || 'N/A'}`); const changeNodes = [...xml.querySelectorAll('Events Tactic')].filter(node => node.getAttribute('teamId') === opponentTid); if (changeNodes.length > 0) { const sortedGoalEvents = this._getSortedGoalEvents(xml); tooltipLines.push('<br><strong>Changes</strong>'); changeNodes.forEach(node => { const changeType = node.getAttribute('type'); if (!['playstyle', 'aggression', 'tactic'].includes(changeType)) return; const clock = node.getAttribute('clock') || node.getAttribute('time'); const timeInSeconds = this._parseTimeToSeconds(clock); const minute = Math.floor(timeInSeconds / 60); const newSetting = node.getAttribute('new_setting'); const { homeScore, awayScore } = this._calculateScoreAtTime(sortedGoalEvents, timeInSeconds, homeTeamId, awayTeamId); const scoreString = isVisiting ? `${awayScore}-${homeScore}` : `${homeScore}-${awayScore}`; tooltipLines.push(`Min ${minute}: ${changeType} → ${newSetting} (Score: ${scoreString})`); }); } const result = tooltipLines.length > 0 ? tooltipLines.join('<br>') : 'No relevant tactical data found.'; return result; }; /** * @description Adds hover functionality to tactic items to show playstyle data. * @param {string} mid - The ID of the match. * @param {string} opponentTid - The ID of the opponent team. * @param {HTMLElement} tacticItemElement - The element representing the tactic. */ addPlaystyleHover = (mid, opponentTid, tacticItemElement) => { const indicatorElement = tacticItemElement.querySelector('.playstyle-status-indicator'); const loadAndShowTooltip = async () => { const cacheKey = `${mid}-${opponentTid}`; const cacheEntry = this.playstyleCache[cacheKey]; if (cacheEntry?.status === 'success' || cacheEntry?.status === 'error') { this.tooltipElement.innerHTML = cacheEntry.content; } else { this.tooltipElement.innerHTML = 'Loading...'; } if (this.tooltipElement.style.display !== 'block') { this.tooltipElement.style.display = 'block'; } if (!cacheEntry || cacheEntry.status === 'error') { this.playstyleCache[cacheKey] = { status: 'loading' }; if (indicatorElement) { indicatorElement.innerHTML = ''; new Spinner({ lines: 8, length: 3, width: 2, radius: 4, scale: 0.5, color: '#FFFFFF', position: 'relative' }).spin(indicatorElement); } try { const content = await this.fetchPlaystyleChanges(mid, opponentTid); this.playstyleCache[cacheKey] = { status: 'success', content }; if (this.tooltipElement.style.display === 'block') this.tooltipElement.innerHTML = content; if (indicatorElement) indicatorElement.innerHTML = '✅'; } catch (error) { const errorMessage = 'Match data is not available. Probably a WO :)'; this.playstyleCache[cacheKey] = { status: 'error', content: errorMessage }; if (this.tooltipElement.style.display === 'block') this.tooltipElement.innerHTML = errorMessage; if (indicatorElement) indicatorElement.innerHTML = '❌'; } } }; tacticItemElement.addEventListener('mouseenter', () => { clearTimeout(this.tooltipHideTimeout); loadAndShowTooltip(); }); tacticItemElement.addEventListener('mousemove', (ev) => { this.tooltipElement.style.top = `${ev.pageY + 15}px`; this.tooltipElement.style.left = `${ev.pageX + 5}px`; }); tacticItemElement.addEventListener('mouseleave', () => { this.tooltipHideTimeout = setTimeout(() => { this.tooltipElement.style.display = 'none'; }, 200); }); }; /** * @description Creates a global tooltip element. */ createGlobalTooltip = () => { this.tooltipElement = document.createElement('div'); this.tooltipElement.className = 'playstyle-tooltip'; document.body.appendChild(this.tooltipElement); this.tooltipElement.addEventListener('mouseenter', () => clearTimeout(this.tooltipHideTimeout)); this.tooltipElement.addEventListener('mouseleave', () => { this.tooltipHideTimeout = setTimeout(() => { this.tooltipElement.style.display = 'none'; }, 200); }); }; /** * @description Waits for ELO values from another script before inserting icons. */ waitForEloValues = () => { const interval = setInterval(() => { const elements = document.querySelectorAll(OpponentTacticsPreview.CONSTANTS.SELECTORS.HOME_TEAM); if (elements.length > 0 && elements[elements.length - 1]?.innerHTML.includes('br')) { clearInterval(interval); this.insertIconsAndListeners(); } }, 100); setTimeout(() => clearInterval(interval), 1500); }; /** * @description Handles global click events for the script's UI. * @param {Event} e - The click event object. */ handleClickEvents = (e) => { const clickedMagnifier = e.target.closest('.magnifier-icon'); if (clickedMagnifier) { e.preventDefault(); e.stopPropagation(); const tid = clickedMagnifier.dataset.tid; const name = clickedMagnifier.dataset.opponent; if (!tid) return; this.saveTeam(tid, name); this.showInteractionModal(tid, clickedMagnifier); return; } const interactionModal = document.getElementById('interaction-modal'); if (interactionModal && !interactionModal.contains(e.target)) { this.fadeOutAndRemove(interactionModal); } const tacticsContainer = document.getElementById('tactics-container'); if (tacticsContainer && !e.target.closest('#tactics-container')) { this.fadeOutAndRemove(tacticsContainer); } }; /** * @description Initializes the user's team ID. * @async */ initUserTeamId = async () => { let storedId = GM_getValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.USER_TEAM_ID); if (storedId) { this.myTeamId = storedId; return; } try { const response = await fetch(OpponentTacticsPreview.CONSTANTS.URLS.CLUBHOUSE); const text = await response.text(); const match = text.match(/dynimg\/badge\.php\?team_id=(\d+)/); if (match && match[1]) { this.myTeamId = match[1]; GM_setValue(OpponentTacticsPreview.CONSTANTS.STORAGE_KEYS.USER_TEAM_ID, this.myTeamId); } } catch (error) {} }; /** * @description Main initialization function for the userscript. * @async */ init = async () => { GM_addStyle(GM_getResourceText('ylotp')); this.createGlobalTooltip(); await this.initUserTeamId(); const statsXenteRunning = document.querySelector(OpponentTacticsPreview.CONSTANTS.SELECTORS.STATS_XENTE); const eloScheduledSelected = document.querySelector(OpponentTacticsPreview.CONSTANTS.SELECTORS.ELO_SCHEDULED)?.checked; if (statsXenteRunning && eloScheduledSelected) { this.waitForEloValues(); } else { this.insertIconsAndListeners(); } this.startObserving(); document.body.addEventListener('click', this.handleClickEvents, true); }; } const otp = new OpponentTacticsPreview(); otp.init(); })(); }).call(window); }); /* mzcopilot:module:[email protected] */ window.__mzcMega.run("weekly-matches", "5.5", function () { (function () { (function () { 'use strict'; GM_addStyle(GM_getResourceText('weeklyMatchesTrackerStyles')); class MZConfig { static get ENDPOINTS() { return { CHALLENGE_TEMPLATE: 'https://www.managerzone.com/ajax.php', MATCHES_LIST: 'https://www.managerzone.com/ajax.php', MANAGER_DATA: 'https://www.managerzone.com/xml/manager_data.php', MATCH_INFO: 'https://www.managerzone.com/xml/match_info.php' }; } static get MATCH_DAYS_FRIENDLY() { return ['d1', 'd3', 'd4', 'd5', 'd7']; } static get SPORT_IDS() { return { SOCCER: '1', HOCKEY: '2' }; } static get LEAGUE_TYPES() { return ['series', 'u18_series', 'u21_series', 'u23_series']; } } class MatchTracker { constructor() { this.initializeState(); this.initializeUI(); this.fetchPageData(); } initializeState() { const sportElement = document.querySelector('#shortcut_link_thezone'); const sportParam = new URL(sportElement.href).searchParams.get('sport'); this.sport = sportParam; this.sportId = sportParam === 'soccer' ? MZConfig.SPORT_IDS.SOCCER : MZConfig.SPORT_IDS.HOCKEY; this.teamId = null; this.appearances = new Map(); this.matchesFetched = { friendly: false, league: false }; this.pendingLeagueFetches = MZConfig.LEAGUE_TYPES.length; this.isLoadingComplete = false; } initializeUI() { this.createModal(); this.createLoadingElements(); this.createTable(); this.addMainButton(); } createModal() { const modal = document.createElement('div'); modal.className = 'friendly-modal'; const content = document.createElement('div'); content.className = 'friendly-modal-content'; const close = document.createElement('span'); close.className = 'friendly-close'; close.innerHTML = '×'; close.onclick = () => this.toggleModal(false); content.appendChild(close); modal.appendChild(content); document.body.appendChild(modal); this.modal = modal; this.modalContent = content; modal.onclick = (e) => { if (e.target === modal) { this.toggleModal(false); } }; } createTable() { this.table = document.createElement('table'); this.table.className = 'friendly-table'; this.table.style.display = 'none'; this.modalContent.appendChild(this.table); } createLoadingElements() { const loadingDiv = document.createElement('div'); loadingDiv.className = 'friendly-loading'; const spinner = document.createElement('div'); spinner.className = 'friendly-spinner'; spinner.innerHTML = '<div></div><div></div><div></div><div></div>'; const message = document.createElement('p'); message.className = 'friendly-message'; loadingDiv.appendChild(spinner); loadingDiv.appendChild(message); this.modalContent.appendChild(loadingDiv); this.loadingDiv = loadingDiv; this.spinner = spinner; this.messageElement = message; } addMainButton() { const checkExist = setInterval(() => { const target = document.getElementById('fss-title-heading'); if (target) { clearInterval(checkExist); const container = document.createElement('div'); container.className = 'friendly-trigger-container'; const text = document.createElement('span'); text.className = 'friendly-text'; text.textContent = 'Click to check total matches played this week ->'; const button = document.createElement('button'); button.className = 'friendly-button'; button.setAttribute('aria-label', 'Show weekly player matches'); button.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zM4.87 4.47a.5.5 0 0 0-.434-.27H3.21a.5.5 0 0 0-.457.672l.62 1.855a.5.5 0 0 0 .457.328h1.22a.5.5 0 0 0 .48-.64L4.87 4.47zM6.16 4.2h.81a.5.5 0 0 0 .48-.64L6.778 1.7a.5.5 0 0 0-.46-.327H5.134a.5.5 0 0 0-.457.672l.62 1.855a.5.5 0 0 0 .457.328h.406zm3.464 0h.81a.5.5 0 0 0 .457-.328l.62-1.855a.5.5 0 0 0-.457-.672H9.23a.5.5 0 0 0-.46.327L8.098 3.56a.5.5 0 0 0 .48.64h.416zM11.67 4.47l-.66 1.98a.5.5 0 0 0 .48.64h1.22a.5.5 0 0 0 .457-.328l.62-1.855a.5.5 0 0 0-.457-.672h-1.22a.5.5 0 0 0-.434.27zM4.183 7.342l-1.15 1.15a.5.5 0 0 0-.044.658l.866 1.298a.5.5 0 0 0 .658.044l1.15-1.15a.5.5 0 0 0 .044-.658L4.84 7.386a.5.5 0 0 0-.658-.044zm1.88 0l-.866 1.299a.5.5 0 0 0 .044.658l1.15 1.15a.5.5 0 0 0 .658.044l.866-1.299a.5.5 0 0 0-.044-.658L6.72 7.386a.5.5 0 0 0-.658-.044zm2.63 0l-.866 1.299a.5.5 0 0 0 .044.658l1.15 1.15a.5.5 0 0 0 .658.044l.866-1.299a.5.5 0 0 0-.044-.658L9.35 7.386a.5.5 0 0 0-.658-.044zm2.48 0l-.866 1.299a.5.5 0 0 0 .044.658l1.15 1.15a.5.5 0 0 0 .658.044l.866-1.299a.5.5 0 0 0-.044-.658l-1.15-1.15a.5.5 0 0 0-.658-.044zm.886 3.03l.866-1.299a.5.5 0 0 0-.044-.658L11.194 7.386a.5.5 0 0 0-.658.044l-.866 1.299a.5.5 0 0 0 .044.658l1.15 1.15a.5.5 0 0 0 .658.044zM4.5 10.05L3.35 8.9a.5.5 0 0 0-.658-.044L1.826 10.155a.5.5 0 0 0-.044.658l.866 1.299a.5.5 0 0 0 .658.044l1.15-1.15a.5.5 0 0 0 .044-.658zm6.836 0l.866 1.299a.5.5 0 0 0 .658.044l1.15-1.15a.5.5 0 0 0 .044-.658l-.866-1.299a.5.5 0 0 0-.658-.044L11.336 8.9a.5.5 0 0 0 .044.658zM7.21 10.692l-1.15 1.15a.5.5 0 0 0-.044.658l.866 1.298a.5.5 0 0 0 .658.044l1.15-1.15a.5.5 0 0 0 .044-.658l-.866-1.299a.5.5 0 0 0-.658-.044zm1.58 0l-.866 1.299a.5.5 0 0 0 .044.658l1.15 1.15a.5.5 0 0 0 .658.044l.866-1.299a.5.5 0 0 0-.044-.658l-1.15-1.15a.5.5 0 0 0-.658-.044z"/></svg>'; button.onclick = () => this.toggleModal(true); container.appendChild(text); container.appendChild(button); target.parentNode.insertBefore(container, target); } }, 100); } toggleModal(show) { requestAnimationFrame(() => { if (show) { this.modal.style.display = 'flex'; requestAnimationFrame(() => { this.modal.classList.add('visible'); if (!this.isLoadingComplete) { this.loadingDiv.style.display = 'flex'; this.spinner.style.display = 'block'; this.messageElement.style.display = 'none'; this.table.style.display = 'none'; } else { this.loadingDiv.style.display = 'none'; this.table.style.display = 'table'; } }); } else { this.modal.classList.remove('visible'); setTimeout(() => { this.modal.style.display = 'none'; if (!this.isLoadingComplete) { this.loadingDiv.style.display = 'none'; } }, 250); } }); } async fetchPageData() { try { const response = await fetch(window.location.href); const data = await response.text(); const doc = new DOMParser().parseFromString(data, 'text/html'); const username = doc.getElementById('header-username').textContent; await this.fetchManagerData(username); } catch (error) { console.warn('Error fetching page data:', error); this.showErrorMessage('Could not fetch page data.'); } } async fetchManagerData(username) { try { const url = new URL(MZConfig.ENDPOINTS.MANAGER_DATA); url.searchParams.set('sport_id', this.sportId); url.searchParams.set('username', username); const response = await fetch(url); if (!response.ok) throw new Error(`Manager data fetch failed: ${response.status}`); const xmlDoc = new DOMParser().parseFromString(await response.text(), 'text/xml'); const teamElement = Array.from(xmlDoc.getElementsByTagName('Team')).find(team => team.getAttribute('sport') === this.sport); if (!teamElement) throw new Error('Could not find team element in manager data.'); this.teamId = teamElement.getAttribute('teamId'); if (!this.teamId) throw new Error('Could not extract team ID from manager data.'); await this.fetchAllMatches(); } catch (error) { console.warn('Error fetching manager data:', error); this.showErrorMessage(`Could not fetch manager data: ${error.message}`); } } checkCompletion() { if (this.matchesFetched.friendly && this.matchesFetched.league) { this.isLoadingComplete = true; if (this.appearances.size === 0) { this.showNoMatchesMessage(); } else { this.displayPlayerMatches(); } } } handleLeagueFetchCompletion() { this.pendingLeagueFetches--; if (this.pendingLeagueFetches <= 0) { this.matchesFetched.league = true; this.checkCompletion(); } } async fetchAllMatches() { this.isLoadingComplete = false; this.appearances.clear(); const friendlyPromise = this.fetchFriendlyMatches().catch(error => { console.warn('Error in fetchFriendlyMatches:', error); }).finally(() => { this.matchesFetched.friendly = true; this.checkCompletion(); }); const leaguePromises = MZConfig.LEAGUE_TYPES.map((leagueType) => this.fetchLeagueMatches(leagueType) .catch((error) => { console.warn(`Error in fetchLeagueMatches for ${leagueType}:`, error); }) .finally(() => { this.handleLeagueFetchCompletion(); }) ); await Promise.all([friendlyPromise, ...leaguePromises]).catch(error => { console.warn('Error fetching one or more match types:', error); this.showErrorMessage('Error fetching some match data.'); }); } async fetchFriendlyMatches() { try { const url = new URL(MZConfig.ENDPOINTS.CHALLENGE_TEMPLATE); url.searchParams.set('p', 'challenge'); url.searchParams.set('sub', 'personal-challenge-template'); url.searchParams.set('sport', this.sport); const response = await fetch(url); if (!response.ok) throw new Error(`Challenge template fetch failed: ${response.status}`); const doc = new DOMParser().parseFromString(await response.text(), 'text/html'); const matchesDiv = this.getCurrentWeekFriendlyMatchesDiv(doc); let friendlyMatchIds = []; if (matchesDiv) { friendlyMatchIds = this.extractFriendlyMatchIds(matchesDiv); } if (friendlyMatchIds.length > 0) { await this.fetchAndProcessMatches(friendlyMatchIds, 'friendly'); } } catch (error) { console.warn('Error fetching friendly matches:', error); } } getCurrentWeekFriendlyMatchesDiv(doc) { const scheduleDiv = doc.getElementById('friendly_series_schedule'); if (!scheduleDiv) return null; const calendarDiv = scheduleDiv.querySelector('.calendar'); if (!calendarDiv) return null; const calendarForm = calendarDiv.querySelector('#saveMatchTactics'); return calendarForm?.querySelector('div.flex-nowrap.fss-row.fss-gw-wrapper.fss-has-matches'); } extractFriendlyMatchIds(matchesDiv) { const extractMatchIdFromLink = (link) => { const href = link.getAttribute('href'); if (!href) return null; const match = href.match(/mid=(\d+)/); return match ? match[1] : null; }; return MZConfig.MATCH_DAYS_FRIENDLY .map((className) => matchesDiv.querySelector(`.${className}`)) .filter(Boolean) .flatMap((div) => Array.from(div.querySelectorAll('a.score-shown:not(.gray)')) .map(extractMatchIdFromLink) .filter(Boolean)); } formatDateForMZ(date) { const d = String(date.getDate()).padStart(2, '0'); const m = String(date.getMonth() + 1).padStart(2, '0'); const y = date.getFullYear(); return `${d}-${m}-${y}`; } getRelevantLeagueDates() { const now = new Date(); const resetHourUTC = 22; const resetDayUTC = 1; const currentDayUTC = now.getUTCDay(); const currentHourUTC = now.getUTCHours(); const startOfCurrentMZWeek = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); startOfCurrentMZWeek.setUTCHours(resetHourUTC, 0, 0, 0); let daysAgo = (currentDayUTC - resetDayUTC + 7) % 7; if (currentDayUTC < resetDayUTC || (currentDayUTC === resetDayUTC && currentHourUTC < resetHourUTC)) { daysAgo += 7; } startOfCurrentMZWeek.setUTCDate(startOfCurrentMZWeek.getUTCDate() - daysAgo); const wednesdayDate = new Date(startOfCurrentMZWeek); wednesdayDate.setUTCDate(startOfCurrentMZWeek.getUTCDate() + 2); const sundayDate = new Date(startOfCurrentMZWeek); sundayDate.setUTCDate(startOfCurrentMZWeek.getUTCDate() + 6); const relevantDates = new Set(); relevantDates.add(this.formatDateForMZ(wednesdayDate)); relevantDates.add(this.formatDateForMZ(sundayDate)); return relevantDates; } async fetchLeagueMatches(selectType) { const relevantDates = this.getRelevantLeagueDates(); if (relevantDates.size === 0) { return; } try { const url = new URL(MZConfig.ENDPOINTS.MATCHES_LIST); url.searchParams.set('p', 'matches'); url.searchParams.set('sub', 'list'); url.searchParams.set('sport', this.sport); const body = new URLSearchParams({ type: 'played', hidescore: 'false', tid1: this.teamId, offset: '', selectType: selectType, limit: '10' }); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' }, body: body.toString() }); if (!response.ok) throw new Error(`League (${selectType}) fetch failed: ${response.status}`); const data = await response.json(); if (!data || !data.list) { console.warn(`Invalid or empty league match list response for ${selectType}`); return; } const doc = new DOMParser().parseFromString(data.list, 'text/html'); const leagueMatchIds = this.extractLeagueMatchIds(doc, relevantDates); if (leagueMatchIds.length > 0) { await this.fetchAndProcessMatches(leagueMatchIds, 'league'); } } catch (error) { console.warn(`Error fetching league matches for type ${selectType}:`, error); throw error; } } extractLeagueMatchIds(doc, relevantDates) { const matchIds = []; const groups = doc.querySelectorAll('#fixtures-results-list > dd.group'); groups.forEach(group => { const dateText = group.textContent.trim(); if (relevantDates.has(dateText)) { let nextElement = group.nextElementSibling; while (nextElement && !nextElement.classList.contains('group')) { if (nextElement.tagName === 'DD' && (nextElement.classList.contains('odd') || nextElement.classList.contains('even'))) { const link = nextElement.querySelector('a.score-shown:not(.gray)'); if (link) { const href = link.getAttribute('href'); if (href) { const match = href.match(/mid=(\d+)/); if (match && match[1]) { matchIds.push(match[1]); } } } } nextElement = nextElement.nextElementSibling; } } }); return matchIds; } async fetchAndProcessMatches(matchIds, type) { if (!matchIds || matchIds.length === 0) { return; } const processSingleMatch = async (matchId) => { try { const url = new URL(MZConfig.ENDPOINTS.MATCH_INFO); url.searchParams.set('sport_id', this.sportId); url.searchParams.set('match_id', matchId); const response = await fetch(url); if (!response.ok) { console.warn(`Failed to fetch match info for ID ${matchId}, status: ${response.status}`); return; } const xmlText = await response.text(); if (!xmlText) { console.warn(`Empty response for match info ID ${matchId}`); return; } const xmlDoc = new DOMParser().parseFromString(xmlText, 'text/xml'); if (xmlDoc.getElementsByTagName("parsererror").length > 0) { console.warn(`Error parsing XML for match info ID ${matchId}`); return; } const teamElements = Array.from(xmlDoc.getElementsByTagName('Team')); const ourTeamElement = teamElements.find(team => team.getAttribute('id') === this.teamId); if (ourTeamElement) { this.updatePlayerAppearances(ourTeamElement, type); } else { console.warn(`Could not find team ${this.teamId} in match ${matchId}`); } } catch (error) { console.warn(`Error processing match ID ${matchId}:`, error); } }; await Promise.all(matchIds.map(matchId => processSingleMatch(matchId))); } updatePlayerAppearances(ourTeamElement, type) { Array.from(ourTeamElement.getElementsByTagName('Player')).forEach(player => { const playerId = player.getAttribute('id'); if (!playerId) return; const playerName = player.getAttribute('name'); let playerInfo = this.appearances.get(playerId); if (!playerInfo) { playerInfo = { name: playerName, friendly: 0, league: 0 }; this.appearances.set(playerId, playerInfo); } if (type === 'friendly') { playerInfo.friendly += 1; } else if (type === 'league') { if (playerInfo.league < 2) { playerInfo.league += 1; } } }); } displayPlayerMatches() { this.loadingDiv.style.display = 'none'; this.spinner.style.display = 'none'; this.messageElement.style.display = 'none'; this.table.style.display = 'table'; this.table.innerHTML = ''; this.table.appendChild(this.createHeaderRow()); const sortedPlayers = Array.from(this.appearances.entries()) .filter(([, playerInfo]) => playerInfo.friendly > 0 || playerInfo.league > 0) .sort(([, a], [, b]) => { const totalA = a.friendly + a.league; const totalB = b.friendly + b.league; if (totalB !== totalA) { return totalB - totalA; } if (b.league !== a.league) { return b.league - a.league; } return b.friendly - a.friendly; }); if (sortedPlayers.length === 0) { this.showNoMatchesMessage(); return; } sortedPlayers.forEach(([playerId, playerInfo]) => { this.table.appendChild(this.createPlayerRow(playerId, playerInfo)); }); } createHeaderRow() { const row = document.createElement('tr'); ['Player', 'Matches'].forEach(text => { const th = document.createElement('th'); th.className = 'friendly-header'; th.textContent = text; row.appendChild(th); }); return row; } createPlayerRow(playerId, playerInfo) { const row = document.createElement('tr'); const nameCell = document.createElement('td'); nameCell.className = 'friendly-cell'; const link = document.createElement('a'); link.className = 'friendly-link'; link.href = `https://www.managerzone.com/?p=players&pid=${playerId}`; link.target = '_blank'; link.textContent = playerInfo.name; nameCell.appendChild(link); const appearancesCell = document.createElement('td'); appearancesCell.className = 'friendly-cell'; const totalMatches = playerInfo.friendly + playerInfo.league; const detailsParts = []; if (playerInfo.friendly > 0) { detailsParts.push(`${playerInfo.friendly} Friendl${playerInfo.friendly === 1 ? 'y' : 'ies'}`); } if (playerInfo.league > 0) { detailsParts.push(`${playerInfo.league} League Match${playerInfo.league === 1 ? '' : 'es'}`); } const detailsString = detailsParts.length > 0 ? `(${detailsParts.join(', ')})` : ''; appearancesCell.innerHTML = ''; const totalSpan = document.createElement('span'); totalSpan.className = 'total-matches'; totalSpan.textContent = totalMatches; appearancesCell.appendChild(totalSpan); if (detailsString) { const detailsSpan = document.createElement('span'); detailsSpan.className = 'match-details'; detailsSpan.textContent = ' ' + detailsString; appearancesCell.appendChild(detailsSpan); } else if (totalMatches === 0) { totalSpan.textContent = '0'; } row.appendChild(nameCell); row.appendChild(appearancesCell); return row; } showNoMatchesMessage() { this.loadingDiv.style.display = 'flex'; this.spinner.style.display = 'none'; this.messageElement.style.display = 'block'; this.messageElement.classList.remove('error'); this.messageElement.textContent = 'No friendly or relevant league matches found for players this week!'; this.table.style.display = 'none'; this.table.innerHTML = ''; } showErrorMessage(message) { this.isLoadingComplete = true; this.loadingDiv.style.display = 'flex'; this.spinner.style.display = 'none'; this.messageElement.style.display = 'block'; this.messageElement.classList.add('error'); this.messageElement.textContent = message; this.table.style.display = 'none'; this.table.innerHTML = ''; } } new MatchTracker(); })(); }).call(window); });