diff --git a/analysis/player_identity_registry.json b/analysis/player_identity_registry.json index b23b4c3..043807e 100644 --- a/analysis/player_identity_registry.json +++ b/analysis/player_identity_registry.json @@ -892,7 +892,9 @@ "names": [ "Lateralus" ], - "team_history": [], + "team_history": [ + "SKY" + ], "first_season": 1, "last_season": 10 }, @@ -925,7 +927,9 @@ "names": [ "gambit" ], - "team_history": [], + "team_history": [ + "TBDi" + ], "first_season": 1, "last_season": 10 }, @@ -1112,7 +1116,9 @@ "names": [ "QuessX" ], - "team_history": [], + "team_history": [ + "ZTi" + ], "first_season": 1, "last_season": 10 }, @@ -1255,7 +1261,9 @@ "names": [ "MrSoap" ], - "team_history": [], + "team_history": [ + "STHX" + ], "first_season": 1, "last_season": 10 }, @@ -1266,7 +1274,9 @@ "names": [ "Mr V" ], - "team_history": [], + "team_history": [ + "TBDi" + ], "first_season": 1, "last_season": 10 }, @@ -1365,7 +1375,9 @@ "names": [ "Universal" ], - "team_history": [], + "team_history": [ + "STHX" + ], "first_season": 1, "last_season": 10 }, @@ -1398,7 +1410,9 @@ "names": [ "Rapscallion" ], - "team_history": [], + "team_history": [ + "STHX" + ], "first_season": 1, "last_season": 10 }, @@ -1442,7 +1456,9 @@ "names": [ "Mando" ], - "team_history": [], + "team_history": [ + "SKY" + ], "first_season": 1, "last_season": 10 }, @@ -1475,7 +1491,9 @@ "names": [ "Venom" ], - "team_history": [], + "team_history": [ + "REKT" + ], "first_season": 1, "last_season": 10 }, @@ -1541,7 +1559,9 @@ "names": [ "STUN" ], - "team_history": [], + "team_history": [ + "REKT" + ], "first_season": 1, "last_season": 10 }, @@ -1552,7 +1572,9 @@ "names": [ "destroyer" ], - "team_history": [], + "team_history": [ + "TANK" + ], "first_season": 1, "last_season": 10 }, @@ -1717,7 +1739,9 @@ "names": [ "firehorse" ], - "team_history": [], + "team_history": [ + "EMU" + ], "first_season": 1, "last_season": 10 }, @@ -1750,7 +1774,9 @@ "names": [ "Spaghetti" ], - "team_history": [], + "team_history": [ + "ZTi" + ], "first_season": 1, "last_season": 10 }, @@ -1959,7 +1985,9 @@ "names": [ "tost" ], - "team_history": [], + "team_history": [ + "VRUS" + ], "first_season": 1, "last_season": 10 }, @@ -1992,7 +2020,9 @@ "names": [ "AdoptionPapers" ], - "team_history": [], + "team_history": [ + "STHX" + ], "first_season": 1, "last_season": 10 }, @@ -2355,7 +2385,9 @@ "names": [ "Hodge" ], - "team_history": [], + "team_history": [ + "LMB0" + ], "first_season": 1, "last_season": 10 }, @@ -2542,7 +2574,9 @@ "names": [ ".com" ], - "team_history": [], + "team_history": [ + "REKT" + ], "first_season": 1, "last_season": 10 }, @@ -2597,7 +2631,9 @@ "names": [ "GarbageMan" ], - "team_history": [], + "team_history": [ + "VRUS" + ], "first_season": 1, "last_season": 10 }, @@ -2608,7 +2644,9 @@ "names": [ "F2P" ], - "team_history": [], + "team_history": [ + "Infi" + ], "first_season": 1, "last_season": 10 }, @@ -2663,7 +2701,9 @@ "names": [ "c2" ], - "team_history": [], + "team_history": [ + "REKT" + ], "first_season": 1, "last_season": 10 }, @@ -2751,7 +2791,9 @@ "names": [ "Bueller" ], - "team_history": [], + "team_history": [ + "EMU" + ], "first_season": 1, "last_season": 10 }, @@ -2762,7 +2804,9 @@ "names": [ "Kanda" ], - "team_history": [], + "team_history": [ + "DRGN" + ], "first_season": 1, "last_season": 10 }, @@ -3224,7 +3268,9 @@ "names": [ "GeleArthur" ], - "team_history": [], + "team_history": [ + "F1R3" + ], "first_season": 1, "last_season": 10 }, @@ -3268,7 +3314,9 @@ "names": [ "Biblecrusader" ], - "team_history": [], + "team_history": [ + "AGGG" + ], "first_season": 1, "last_season": 10 }, @@ -3279,7 +3327,9 @@ "names": [ "amospalla" ], - "team_history": [], + "team_history": [ + "TBDi" + ], "first_season": 1, "last_season": 10 }, @@ -3323,7 +3373,9 @@ "names": [ "Fractor" ], - "team_history": [], + "team_history": [ + "TANK" + ], "first_season": 1, "last_season": 10 }, @@ -3477,7 +3529,9 @@ "names": [ "damageu" ], - "team_history": [], + "team_history": [ + "STHX" + ], "first_season": 1, "last_season": 10 }, @@ -3829,7 +3883,9 @@ "names": [ "Gold" ], - "team_history": [], + "team_history": [ + "TBDi" + ], "first_season": 2, "last_season": 10 }, @@ -3884,7 +3940,9 @@ "names": [ "realityforge" ], - "team_history": [], + "team_history": [ + "STHX" + ], "first_season": 2, "last_season": 10 }, @@ -3895,7 +3953,9 @@ "names": [ "sirvoid" ], - "team_history": [], + "team_history": [ + "ZTi" + ], "first_season": 2, "last_season": 10 }, @@ -3972,7 +4032,9 @@ "names": [ "AXiON" ], - "team_history": [], + "team_history": [ + "STHX" + ], "first_season": 2, "last_season": 10 }, @@ -4082,7 +4144,9 @@ "names": [ "Birky" ], - "team_history": [], + "team_history": [ + "FAM!" + ], "first_season": 2, "last_season": 10 }, @@ -4192,7 +4256,9 @@ "names": [ "Noble" ], - "team_history": [], + "team_history": [ + "F1R3" + ], "first_season": 3, "last_season": 10 }, @@ -4236,7 +4302,9 @@ "names": [ "DarkMoonRDF" ], - "team_history": [], + "team_history": [ + "AGGG" + ], "first_season": 3, "last_season": 10 }, @@ -4247,7 +4315,9 @@ "names": [ "Reko" ], - "team_history": [], + "team_history": [ + "TBDi" + ], "first_season": 3, "last_season": 10 }, @@ -4621,7 +4691,9 @@ "names": [ "Quokka" ], - "team_history": [], + "team_history": [ + "STHX" + ], "first_season": 4, "last_season": 10 }, @@ -4819,7 +4891,9 @@ "names": [ "Rocky-James" ], - "team_history": [], + "team_history": [ + "HAXX" + ], "first_season": 7, "last_season": 10 }, @@ -4841,7 +4915,9 @@ "names": [ "Vanguard" ], - "team_history": [], + "team_history": [ + "HAXX" + ], "first_season": 7, "last_season": 10 }, @@ -4852,7 +4928,9 @@ "names": [ "Emperor Kars" ], - "team_history": [], + "team_history": [ + "ZTi" + ], "first_season": 7, "last_season": 10 }, @@ -4863,7 +4941,9 @@ "names": [ "the.suit.maker" ], - "team_history": [], + "team_history": [ + "Infi" + ], "first_season": 7, "last_season": 10 }, @@ -4940,7 +5020,9 @@ "names": [ "Shout" ], - "team_history": [], + "team_history": [ + "TANK" + ], "first_season": 7, "last_season": 10 }, @@ -4962,7 +5044,9 @@ "names": [ "Dashhole" ], - "team_history": [], + "team_history": [ + "LMB0" + ], "first_season": 7, "last_season": 10 }, @@ -5050,7 +5134,9 @@ "names": [ "Iconic" ], - "team_history": [], + "team_history": [ + "just" + ], "first_season": 7, "last_season": 10 }, @@ -5083,7 +5169,9 @@ "names": [ "snarples" ], - "team_history": [], + "team_history": [ + "SKY" + ], "first_season": 7, "last_season": 10 }, @@ -5094,7 +5182,9 @@ "names": [ "wanderer" ], - "team_history": [], + "team_history": [ + "SKY" + ], "first_season": 7, "last_season": 10 }, @@ -5171,7 +5261,9 @@ "names": [ "Dazlah" ], - "team_history": [], + "team_history": [ + "DRGN" + ], "first_season": 7, "last_season": 10 }, @@ -5325,7 +5417,9 @@ "names": [ "vewn" ], - "team_history": [], + "team_history": [ + "AK47" + ], "first_season": 7, "last_season": 10 }, @@ -5369,7 +5463,9 @@ "names": [ "Paladin Thor" ], - "team_history": [], + "team_history": [ + "LMB0" + ], "first_season": 8, "last_season": 10 }, @@ -5490,7 +5586,9 @@ "names": [ "D999" ], - "team_history": [], + "team_history": [ + "HAXX" + ], "first_season": 9, "last_season": 10 }, @@ -5699,7 +5797,9 @@ "names": [ "carrot" ], - "team_history": [], + "team_history": [ + "FAM!" + ], "first_season": 13, "last_season": 10 }, @@ -5776,7 +5876,9 @@ "names": [ "LobsterMaster" ], - "team_history": [], + "team_history": [ + "SKY" + ], "first_season": 13, "last_season": 10 }, @@ -5864,7 +5966,9 @@ "names": [ "Xyota" ], - "team_history": [], + "team_history": [ + "DRGN" + ], "first_season": 13, "last_season": 10 }, @@ -5908,7 +6012,9 @@ "names": [ "ElDeaffy" ], - "team_history": [], + "team_history": [ + "REKT" + ], "first_season": 13, "last_season": 10 }, @@ -6106,7 +6212,9 @@ "names": [ "Joneas Jostar" ], - "team_history": [], + "team_history": [ + "Infi" + ], "first_season": 13, "last_season": 10 }, @@ -6183,7 +6291,9 @@ "names": [ "Grim!" ], - "team_history": [], + "team_history": [ + "Infi" + ], "first_season": 14, "last_season": 10 }, @@ -6238,7 +6348,9 @@ "names": [ "Blister22" ], - "team_history": [], + "team_history": [ + "SKY" + ], "first_season": 14, "last_season": 10 }, @@ -6260,7 +6372,9 @@ "names": [ "Clairvoyance" ], - "team_history": [], + "team_history": [ + "ZTi" + ], "first_season": 14, "last_season": 10 }, @@ -6282,7 +6396,9 @@ "names": [ "JohnWilksBoothNickle_Monster" ], - "team_history": [], + "team_history": [ + "AK47" + ], "first_season": 14, "last_season": 10 }, @@ -6337,7 +6453,9 @@ "names": [ "Speed" ], - "team_history": [], + "team_history": [ + "ZTi" + ], "first_season": 15, "last_season": 10 }, @@ -6359,7 +6477,9 @@ "names": [ "Humbles" ], - "team_history": [], + "team_history": [ + "AK47" + ], "first_season": 15, "last_season": 10 }, @@ -6447,7 +6567,9 @@ "names": [ "Sugar" ], - "team_history": [], + "team_history": [ + "REKT" + ], "first_season": 16, "last_season": 10 }, @@ -6458,7 +6580,9 @@ "names": [ "Kyron" ], - "team_history": [], + "team_history": [ + "F1R3" + ], "first_season": 16, "last_season": 10 }, @@ -6568,7 +6692,9 @@ "names": [ "SmolQuadd" ], - "team_history": [], + "team_history": [ + "DMND" + ], "first_season": 19, "last_season": 10 }, @@ -6579,7 +6705,9 @@ "names": [ "Greedy" ], - "team_history": [], + "team_history": [ + "SLNT" + ], "first_season": 19, "last_season": 10 }, @@ -6643,9 +6771,12 @@ "fe6719f1-533d-4a18-a09c-51e155a0d267" ], "names": [ - "2Goose2Furious" + "2Goose2Furious", + "Buzzard Goose" + ], + "team_history": [ + "SKY" ], - "team_history": [], "first_season": 19, "last_season": 10 }, @@ -6788,7 +6919,9 @@ "names": [ "Hog Rider673" ], - "team_history": [], + "team_history": [ + "just" + ], "first_season": 19, "last_season": 10 }, @@ -6821,7 +6954,9 @@ "names": [ "ukmatty" ], - "team_history": [], + "team_history": [ + "HAXX" + ], "first_season": 19, "last_season": 10 }, @@ -6843,7 +6978,9 @@ "names": [ "Drevni" ], - "team_history": [], + "team_history": [ + "HAXX" + ], "first_season": 19, "last_season": 10 }, @@ -6854,7 +6991,9 @@ "names": [ "Romeo" ], - "team_history": [], + "team_history": [ + "DRGN" + ], "first_season": 19, "last_season": 10 }, @@ -6876,7 +7015,9 @@ "names": [ "warnurse" ], - "team_history": [], + "team_history": [ + "EMU" + ], "first_season": 19, "last_season": 10 }, @@ -7019,7 +7160,9 @@ "names": [ "lali Ali" ], - "team_history": [], + "team_history": [ + "HAXX" + ], "first_season": 20, "last_season": 10 }, @@ -7052,7 +7195,9 @@ "names": [ "Mig" ], - "team_history": [], + "team_history": [ + "EMU" + ], "first_season": 21, "last_season": 10 }, @@ -7085,7 +7230,9 @@ "names": [ "KBOMB" ], - "team_history": [], + "team_history": [ + "SKY" + ], "first_season": 22, "last_season": 10 }, @@ -7140,7 +7287,9 @@ "names": [ "8aron" ], - "team_history": [], + "team_history": [ + "REKT" + ], "first_season": 25, "last_season": 10 }, @@ -7151,7 +7300,9 @@ "names": [ "Rawhawd" ], - "team_history": [], + "team_history": [ + "LMB0" + ], "first_season": 26, "last_season": 10 }, @@ -7195,7 +7346,9 @@ "names": [ "Whopper" ], - "team_history": [], + "team_history": [ + "Infi" + ], "first_season": 26, "last_season": 10 }, @@ -7250,7 +7403,9 @@ "names": [ "Toshiro" ], - "team_history": [], + "team_history": [ + "MEX" + ], "first_season": 26, "last_season": 10 }, @@ -7261,7 +7416,9 @@ "names": [ "Lazy boy" ], - "team_history": [], + "team_history": [ + "MEX" + ], "first_season": 26, "last_season": 10 }, @@ -7272,7 +7429,9 @@ "names": [ "DIABLO" ], - "team_history": [], + "team_history": [ + "MEX" + ], "first_season": 26, "last_season": 10 }, @@ -7283,7 +7442,9 @@ "names": [ "Escanor96" ], - "team_history": [], + "team_history": [ + "MEX" + ], "first_season": 26, "last_season": 10 }, @@ -7294,7 +7455,9 @@ "names": [ "HDZ" ], - "team_history": [], + "team_history": [ + "RAZA" + ], "first_season": 26, "last_season": 10 }, @@ -7371,7 +7534,9 @@ "names": [ "PAPI" ], - "team_history": [], + "team_history": [ + "RAZA" + ], "first_season": 26, "last_season": 10 }, @@ -7525,7 +7690,9 @@ "names": [ "Giggles" ], - "team_history": [], + "team_history": [ + "SKY" + ], "first_season": 26, "last_season": 10 }, @@ -7558,7 +7725,9 @@ "names": [ "Solar" ], - "team_history": [], + "team_history": [ + "REKT" + ], "first_season": 26, "last_season": 10 }, @@ -7602,7 +7771,9 @@ "names": [ "RaederGator" ], - "team_history": [], + "team_history": [ + "SKY" + ], "first_season": 27, "last_season": 10 }, @@ -7613,7 +7784,9 @@ "names": [ "buns_bunni" ], - "team_history": [], + "team_history": [ + "LMB0" + ], "first_season": 27, "last_season": 10 }, @@ -7646,7 +7819,9 @@ "names": [ "Huqo.suave" ], - "team_history": [], + "team_history": [ + "SLNT" + ], "first_season": 27, "last_season": 10 }, @@ -7657,7 +7832,9 @@ "names": [ "Lullaby" ], - "team_history": [], + "team_history": [ + "AGGG" + ], "first_season": 27, "last_season": 10 }, @@ -7668,7 +7845,9 @@ "names": [ "Gremlin3529" ], - "team_history": [], + "team_history": [ + "EMRD" + ], "first_season": 28, "last_season": 10 }, @@ -7745,7 +7924,9 @@ "names": [ "Lunar" ], - "team_history": [], + "team_history": [ + "LMB0" + ], "first_season": 28, "last_season": 10 }, @@ -7789,7 +7970,9 @@ "names": [ "albertodd30" ], - "team_history": [], + "team_history": [ + "MEX" + ], "first_season": 29, "last_season": 10 }, @@ -7800,7 +7983,9 @@ "names": [ "Zooinyou" ], - "team_history": [], + "team_history": [ + "just" + ], "first_season": 29, "last_season": 10 }, @@ -7866,7 +8051,9 @@ "names": [ "Arcane!" ], - "team_history": [], + "team_history": [ + "EMU" + ], "first_season": 32, "last_season": 10 }, @@ -7910,7 +8097,9 @@ "names": [ "god dream" ], - "team_history": [], + "team_history": [ + "DRGN" + ], "first_season": 32, "last_season": 10 }, @@ -7965,7 +8154,9 @@ "names": [ "Vega" ], - "team_history": [], + "team_history": [ + "SKY" + ], "first_season": 32, "last_season": 10 }, @@ -7987,7 +8178,9 @@ "names": [ "seasons" ], - "team_history": [], + "team_history": [ + "F1R3" + ], "first_season": 32, "last_season": 10 }, @@ -7998,7 +8191,9 @@ "names": [ "kaha" ], - "team_history": [], + "team_history": [ + "F1R3" + ], "first_season": 32, "last_season": 10 }, @@ -8020,7 +8215,9 @@ "names": [ "echo" ], - "team_history": [], + "team_history": [ + "ZTi" + ], "first_season": 32, "last_season": 10 }, @@ -8042,7 +8239,9 @@ "names": [ "Ivan" ], - "team_history": [], + "team_history": [ + "DMND" + ], "first_season": 32, "last_season": 10 }, @@ -8075,7 +8274,9 @@ "names": [ "Dougi" ], - "team_history": [], + "team_history": [ + "F1R3" + ], "first_season": 32, "last_season": 10 }, @@ -8086,7 +8287,9 @@ "names": [ "(ded)Dave" ], - "team_history": [], + "team_history": [ + "EMU" + ], "first_season": 32, "last_season": 10 }, @@ -8119,7 +8322,9 @@ "names": [ "Sixty9" ], - "team_history": [], + "team_history": [ + "HAXX" + ], "first_season": 32, "last_season": 10 }, @@ -8130,7 +8335,9 @@ "names": [ "Blolbfish" ], - "team_history": [], + "team_history": [ + "TANK" + ], "first_season": 32, "last_season": 10 }, @@ -8152,7 +8359,9 @@ "names": [ "Luketimusprime" ], - "team_history": [], + "team_history": [ + "DMND" + ], "first_season": 33, "last_season": 10 }, @@ -8196,7 +8405,9 @@ "names": [ "Prometheus" ], - "team_history": [], + "team_history": [ + "HAXX" + ], "first_season": 33, "last_season": 10 }, @@ -8229,7 +8440,9 @@ "names": [ "GVR" ], - "team_history": [], + "team_history": [ + "EMRD" + ], "first_season": 35, "last_season": 10 }, @@ -8295,7 +8508,9 @@ "names": [ "THE-REL" ], - "team_history": [], + "team_history": [ + "EMRD" + ], "first_season": 35, "last_season": 10 }, @@ -8317,7 +8532,9 @@ "names": [ "SavitarXI" ], - "team_history": [], + "team_history": [ + "Infi" + ], "first_season": 35, "last_season": 10 }, @@ -8339,7 +8556,9 @@ "names": [ "Gladiator" ], - "team_history": [], + "team_history": [ + "AGGG" + ], "first_season": 35, "last_season": 10 }, @@ -8361,7 +8580,9 @@ "names": [ "K0d14c" ], - "team_history": [], + "team_history": [ + "EMU" + ], "first_season": 35, "last_season": 10 }, @@ -8383,7 +8604,9 @@ "names": [ "SilentKiller_09" ], - "team_history": [], + "team_history": [ + "SLNT" + ], "first_season": 36, "last_season": 10 }, @@ -8449,7 +8672,9 @@ "names": [ "Blitzer" ], - "team_history": [], + "team_history": [ + "F1R3" + ], "first_season": 48, "last_season": 10 }, @@ -8471,7 +8696,9 @@ "names": [ "Vleerbeer420" ], - "team_history": [], + "team_history": [ + "F1R3" + ], "first_season": 48, "last_season": 10 }, @@ -8537,7 +8764,9 @@ "names": [ "LittleOwl" ], - "team_history": [], + "team_history": [ + "AGGG" + ], "first_season": 48, "last_season": 10 }, @@ -8548,7 +8777,9 @@ "names": [ "Baba yaga" ], - "team_history": [], + "team_history": [ + "EMRD" + ], "first_season": 48, "last_season": 10 }, @@ -8570,7 +8801,9 @@ "names": [ "ZELDA" ], - "team_history": [], + "team_history": [ + "RAZA" + ], "first_season": 48, "last_season": 10 }, @@ -8647,7 +8880,9 @@ "names": [ " Johnny" ], - "team_history": [], + "team_history": [ + "SWUA" + ], "first_season": 48, "last_season": 10 }, @@ -8658,7 +8893,9 @@ "names": [ "Rkoti" ], - "team_history": [], + "team_history": [ + "F1R3" + ], "first_season": 48, "last_season": 10 }, @@ -8691,7 +8928,9 @@ "names": [ "Kit" ], - "team_history": [], + "team_history": [ + "SWUA" + ], "first_season": 48, "last_season": 10 }, @@ -8757,7 +8996,9 @@ "names": [ "!PIKATHUNDER!" ], - "team_history": [], + "team_history": [ + "Doc!" + ], "first_season": 48, "last_season": 10 }, @@ -8779,7 +9020,9 @@ "names": [ "HACKERMAN" ], - "team_history": [], + "team_history": [ + "VRUS" + ], "first_season": 48, "last_season": 10 }, @@ -8790,7 +9033,9 @@ "names": [ "Riddler??" ], - "team_history": [], + "team_history": [ + "FAM!" + ], "first_season": 48, "last_season": 10 }, @@ -8801,7 +9046,9 @@ "names": [ "SubHuman" ], - "team_history": [], + "team_history": [ + "FAM!" + ], "first_season": 48, "last_season": 10 }, @@ -8812,7 +9059,9 @@ "names": [ "Smashinghard" ], - "team_history": [], + "team_history": [ + "VRUS" + ], "first_season": 48, "last_season": 10 }, @@ -8823,7 +9072,9 @@ "names": [ "Badger" ], - "team_history": [], + "team_history": [ + "FAM!" + ], "first_season": 48, "last_season": 10 }, @@ -8889,7 +9140,9 @@ "names": [ "Lainey" ], - "team_history": [], + "team_history": [ + "FAM!" + ], "first_season": 48, "last_season": 10 }, @@ -8944,7 +9197,9 @@ "names": [ "TheLetter_S" ], - "team_history": [], + "team_history": [ + "DMND" + ], "first_season": 48, "last_season": 10 }, @@ -8955,7 +9210,9 @@ "names": [ "Ripper" ], - "team_history": [], + "team_history": [ + "EMRD" + ], "first_season": 48, "last_season": 10 }, @@ -8988,7 +9245,9 @@ "names": [ "Alt-f4" ], - "team_history": [], + "team_history": [ + "AGGG" + ], "first_season": 48, "last_season": 10 }, @@ -8999,7 +9258,9 @@ "names": [ "Leo" ], - "team_history": [], + "team_history": [ + "DRGN" + ], "first_season": 48, "last_season": 10 }, @@ -9120,7 +9381,9 @@ "names": [ "Exborboss" ], - "team_history": [], + "team_history": [ + "SWUA" + ], "first_season": 48, "last_season": 10 }, @@ -9142,7 +9405,9 @@ "names": [ "FireStorm " ], - "team_history": [], + "team_history": [ + "AK47" + ], "first_season": 48, "last_season": 10 }, @@ -9186,7 +9451,9 @@ "names": [ "DreDarkroom " ], - "team_history": [], + "team_history": [ + "VRUS" + ], "first_season": 49, "last_season": 10 }, @@ -9219,7 +9486,9 @@ "names": [ "OVERKILL" ], - "team_history": [], + "team_history": [ + "AK47" + ], "first_season": 49, "last_season": 10 }, @@ -9230,7 +9499,9 @@ "names": [ "Ganja75" ], - "team_history": [], + "team_history": [ + "FAM!" + ], "first_season": 49, "last_season": 10 }, @@ -9285,7 +9556,9 @@ "names": [ "Asya " ], - "team_history": [], + "team_history": [ + "SWUA" + ], "first_season": 49, "last_season": 10 }, @@ -9296,7 +9569,9 @@ "names": [ "da-san-man1" ], - "team_history": [], + "team_history": [ + "AK47" + ], "first_season": 49, "last_season": 10 }, @@ -9307,7 +9582,9 @@ "names": [ "Quendi" ], - "team_history": [], + "team_history": [ + "VRUS" + ], "first_season": 49, "last_season": 10 }, @@ -9329,7 +9606,9 @@ "names": [ "DragonPrincess" ], - "team_history": [], + "team_history": [ + "RVNG" + ], "first_season": 49, "last_season": 10 }, @@ -9340,7 +9619,9 @@ "names": [ "Fluffy Bastard" ], - "team_history": [], + "team_history": [ + "EMRD" + ], "first_season": 50, "last_season": 10 }, @@ -9351,7 +9632,9 @@ "names": [ "Ridders1561" ], - "team_history": [], + "team_history": [ + "HAXX" + ], "first_season": 50, "last_season": 10 }, @@ -9505,7 +9788,9 @@ "names": [ "GODDirito" ], - "team_history": [], + "team_history": [ + "LMB0" + ], "first_season": 51, "last_season": 10 }, @@ -9549,7 +9834,9 @@ "names": [ "TardeR0N" ], - "team_history": [], + "team_history": [ + "LMB0" + ], "first_season": 51, "last_season": 10 }, @@ -9571,7 +9858,9 @@ "names": [ "TeTurtleMan" ], - "team_history": [], + "team_history": [ + "LMB0" + ], "first_season": 52, "last_season": 10 }, @@ -9615,7 +9904,9 @@ "names": [ "Cake" ], - "team_history": [], + "team_history": [ + "Infi" + ], "first_season": 53, "last_season": 10 }, @@ -9637,7 +9928,9 @@ "names": [ "pylhoff" ], - "team_history": [], + "team_history": [ + "VRUS" + ], "first_season": 59, "last_season": 10 }, @@ -9648,7 +9941,9 @@ "names": [ "Red3109" ], - "team_history": [], + "team_history": [ + "VRUS" + ], "first_season": 59, "last_season": 10 }, @@ -9659,7 +9954,9 @@ "names": [ "Delusional" ], - "team_history": [], + "team_history": [ + "RIPR" + ], "first_season": 59, "last_season": 10 }, @@ -9681,7 +9978,9 @@ "names": [ "Golden Drgn" ], - "team_history": [], + "team_history": [ + "HOBO" + ], "first_season": 59, "last_season": 10 }, @@ -9692,7 +9991,9 @@ "names": [ "ICE TEA " ], - "team_history": [], + "team_history": [ + "HOBO" + ], "first_season": 59, "last_season": 10 }, @@ -9747,7 +10048,9 @@ "names": [ "Dark Angel" ], - "team_history": [], + "team_history": [ + "Doc!" + ], "first_season": 59, "last_season": 10 }, @@ -9758,7 +10061,9 @@ "names": [ "| @L!3N R3@P3R |" ], - "team_history": [], + "team_history": [ + "SLNT" + ], "first_season": 59, "last_season": 10 }, @@ -9769,7 +10074,9 @@ "names": [ "HATE" ], - "team_history": [], + "team_history": [ + "SWUA" + ], "first_season": 59, "last_season": 10 }, @@ -9780,7 +10087,9 @@ "names": [ "RIP" ], - "team_history": [], + "team_history": [ + "Infi" + ], "first_season": 59, "last_season": 10 }, @@ -9802,7 +10111,9 @@ "names": [ "PURPLE PANDA" ], - "team_history": [], + "team_history": [ + "EMRD" + ], "first_season": 59, "last_season": 10 }, @@ -9824,7 +10135,9 @@ "names": [ "098" ], - "team_history": [], + "team_history": [ + "DRGN" + ], "first_season": 59, "last_season": 10 }, @@ -9835,7 +10148,9 @@ "names": [ "Toast" ], - "team_history": [], + "team_history": [ + "F1R3" + ], "first_season": 59, "last_season": 10 }, @@ -9857,7 +10172,9 @@ "names": [ "Mazzoner" ], - "team_history": [], + "team_history": [ + "LMB0" + ], "first_season": 59, "last_season": 10 }, @@ -9868,7 +10185,9 @@ "names": [ "forestfolk87" ], - "team_history": [], + "team_history": [ + "RVNG" + ], "first_season": 59, "last_season": 10 }, @@ -9901,7 +10220,9 @@ "names": [ "DIEHARD " ], - "team_history": [], + "team_history": [ + "FAM!" + ], "first_season": 59, "last_season": 10 }, @@ -9912,7 +10233,9 @@ "names": [ "Jaker" ], - "team_history": [], + "team_history": [ + "DMND" + ], "first_season": 59, "last_season": 10 }, @@ -9923,7 +10246,9 @@ "names": [ "Schwendel" ], - "team_history": [], + "team_history": [ + "F1R3" + ], "first_season": 59, "last_season": 10 }, @@ -9934,7 +10259,9 @@ "names": [ "FOXYSAMURAI93" ], - "team_history": [], + "team_history": [ + "EMRD" + ], "first_season": 59, "last_season": 10 }, @@ -9945,7 +10272,9 @@ "names": [ "(**)reSonance" ], - "team_history": [], + "team_history": [ + "DMND" + ], "first_season": 59, "last_season": 10 }, @@ -9956,7 +10285,9 @@ "names": [ "Aleisha" ], - "team_history": [], + "team_history": [ + "SWUA" + ], "first_season": 59, "last_season": 10 }, @@ -9967,7 +10298,9 @@ "names": [ "Legoman021" ], - "team_history": [], + "team_history": [ + "FAM!" + ], "first_season": 59, "last_season": 10 }, @@ -9976,9 +10309,12 @@ "11c31fa0-3356-4b71-b2c0-52e8633e227e" ], "names": [ - "Venyum jbVenyum jbVenyum" + "Venyum jbVenyum jbVenyum", + "Venyum jb" + ], + "team_history": [ + "DMND" ], - "team_history": [], "first_season": 59, "last_season": 10 }, @@ -10000,7 +10336,9 @@ "names": [ "Madalyn " ], - "team_history": [], + "team_history": [ + "RVNG" + ], "first_season": 59, "last_season": 10 }, @@ -10044,7 +10382,9 @@ "names": [ "Waterboy" ], - "team_history": [], + "team_history": [ + "just" + ], "first_season": 60, "last_season": 10 }, @@ -10055,7 +10395,9 @@ "names": [ "SOUL" ], - "team_history": [], + "team_history": [ + "STHX" + ], "first_season": 60, "last_season": 10 }, @@ -10066,7 +10408,9 @@ "names": [ "Brotherboy" ], - "team_history": [], + "team_history": [ + "VRUS" + ], "first_season": 60, "last_season": 10 }, @@ -10132,7 +10476,9 @@ "names": [ "Stranger1430" ], - "team_history": [], + "team_history": [ + "EMRD" + ], "first_season": 61, "last_season": 10 }, @@ -10143,7 +10489,9 @@ "names": [ "retsiniSIM WATCHING U" ], - "team_history": [], + "team_history": [ + "DMND" + ], "first_season": 61, "last_season": 10 }, @@ -10209,7 +10557,9 @@ "names": [ "Candyyy" ], - "team_history": [], + "team_history": [ + "Doc!" + ], "first_season": 62, "last_season": 10 }, @@ -10220,7 +10570,9 @@ "names": [ "Hades" ], - "team_history": [], + "team_history": [ + "SWUA" + ], "first_season": 62, "last_season": 10 }, @@ -10231,7 +10583,9 @@ "names": [ "given" ], - "team_history": [], + "team_history": [ + "AK47" + ], "first_season": 62, "last_season": 10 }, @@ -10242,7 +10596,9 @@ "names": [ "Shameless" ], - "team_history": [], + "team_history": [ + "FAM!" + ], "first_season": 62, "last_season": 10 }, @@ -10262,7 +10618,8 @@ "1c776ec6-d244-428b-9a43-65f939e75c41" ], "names": [ - "CRAZY TOILET OG67" + "CRAZY TOILET OG67", + "idkCRAZY KILLER" ], "team_history": [], "first_season": 62, @@ -10275,7 +10632,9 @@ "names": [ "Lelu" ], - "team_history": [], + "team_history": [ + "Infi" + ], "first_season": 62, "last_season": 10 }, @@ -10319,7 +10678,9 @@ "names": [ "Ignite '-'" ], - "team_history": [], + "team_history": [ + "SWUA" + ], "first_season": 64, "last_season": 10 }, diff --git a/analysis/rookie_leaderboard.py b/analysis/rookie_leaderboard.py index 0d9e803..d5da69e 100644 --- a/analysis/rookie_leaderboard.py +++ b/analysis/rookie_leaderboard.py @@ -1,98 +1,92 @@ import os import json +import re +# Paths PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CAREER_DB_PATH = os.path.join(PROJECT_ROOT, "career_stats.json") - -# --------------------------------------------------------- -# Load DB -# --------------------------------------------------------- - def load_career_db(): - if not os.path.exists(CAREER_DB_PATH): - return None + if not os.path.exists(CAREER_DB_PATH): return None with open(CAREER_DB_PATH, "r", encoding="utf-8") as f: return json.load(f) - -# --------------------------------------------------------- -# Identify rookies -# --------------------------------------------------------- - def get_rookies(db, season): rookies = {} - + current_season_int = int(season) + for pid, p in db.get("players", {}).items(): - seasons_played = p.get("seasons_played") or "" - season_list = [int(s) for s in seasons_played.split(",") if s.isdigit()] + career = p.get("career", {}) + maps_played = career.get("maps", 0) + kills = career.get("kills", 0) + + # Determine if they are active this season + matches = p.get("matches", []) + played_this_season = any(int(m.get("CycleID", 0)) >= current_season_int for m in matches) + + # Registry check + reg_first = p.get("first_season") + + # --- THE TRIPLE LOCK --- + # 1. Must be active now + # 2. Maps must be low (< 50) + # 3. Kills must be low (< 800) - This catches veterans with missing history + is_rookie = False + if played_this_season and maps_played < 50 and kills < 1000: + # Final check: Registry shouldn't show them in a previous season + if reg_first is None or int(reg_first) >= current_season_int: + is_rookie = True - # True rookie = only played this season - if len(season_list) == 1 and season_list[0] == season: + if is_rookie: rookies[pid] = p - return rookies +# analysis/rookie_leaderboard.py -# --------------------------------------------------------- -# Rank rookies by a selected tag -# --------------------------------------------------------- - -def rank_rookies_by_tag(season, tag_name, limit=10): - """ - Rank rookies using REAL career stats. - """ - +def rank_rookies_by_tag(season, tag_name, limit=15): db = load_career_db() - if db is None: - return [] - + if db is None: return [] rookies = get_rookies(db, season) - + rows = [] for pid, p in rookies.items(): - - # Pull stats from career DB (correct source) - kills = p.get("kills", 0) - deaths = p.get("deaths", 0) - score = p.get("score", 0) - - # KD - if deaths > 0: - kd = kills / deaths - else: - kd = kills - - # Raw score = KD (for now) - raw = kd - - # Percentile placeholder (we can compute real percentiles later) - pct = raw - - # Team - team = "Unknown" - if p.get("team_history"): - team = p["team_history"][-1] + career = p.get("career", {}) + + # Pull core stats + kills_val = career.get("kills", 0) + kd_val = career.get("KD", 0) + score_val = career.get("score", 0) + maps_val = career.get("maps", 0) + + # Team handling: use history, then registry, then Free Agent + history = p.get("team_history", []) + team_name = "Free Agent" + + # Search backward through history for a non-UUID name + for t in reversed(history): + if t and not (len(str(t)) > 20 and "-" in str(t)): + team_name = str(t) + break + + # If still Free Agent, check if the player object has a 'team' key + # (often populated during Sync) + if team_name == "Free Agent" and p.get("team"): + team_name = p.get("team") rows.append({ "id": pid, "name": p.get("name", "Unknown"), - "team": team, - "pct": pct, - "tier": "C", # placeholder until rookie tiers are added - "raw": raw, - "kd": kd, - "kills": kills, - "deaths": deaths, - "score": score, - "summary": "", + "team": team_name, + "tier": p.get(tag_name, {}).get("tier", "D"), + "kd": kd_val, + "kills": kills_val, + "maps": maps_val, + "score": score_val }) - # Sort by raw score - rows.sort(key=lambda x: x["raw"], reverse=True) - - # Assign ranks + # Sort by Score instead of Rating + rows.sort(key=lambda x: x["score"], reverse=True) + for i, r in enumerate(rows, start=1): r["rank"] = i - return rows[:limit] \ No newline at end of file diff --git a/dashleague_cast_tool.py b/dashleague_cast_tool.py index d7f01ee..3f1e908 100644 --- a/dashleague_cast_tool.py +++ b/dashleague_cast_tool.py @@ -335,6 +335,21 @@ class DashLeagueGUI: # Resolve canonical + stamp seasons canonical = pir.resolve(raw_id, season=self.current_season) + + canonical = pir.resolve(raw_id, season=self.current_season) + + # Update Registry and History with clean names + clean_name = p.get("name") + clean_team = (p.get("team") or "").strip() + + pir.update_name(canonical, clean_name) + if clean_team: + pir.update_team(canonical, clean_team) + + # Ensure career_stats has this history + if canonical in players_db: + players_db[canonical]["team_history"] = pir.data["canonical"][canonical]["team_history"] + if not canonical: continue @@ -377,7 +392,12 @@ class DashLeagueGUI: "breaker": {}, } - pdata = players_db.get(canonical, {}) + pdata = players_db.setdefault(canonical, {}) + pdata["seasons_played"] = merged["seasons_played"] + + print("DEBUG: seasons fields:") + #for p in stats: + # print(p.get("name"), "seasons:", p.get("seasons"), "seasons_played:", p.get("seasons_played")) # Store basic stats into career DB so rookies have real data @@ -401,7 +421,7 @@ class DashLeagueGUI: #if merged.get("seasons_played"): # pdata["seasons_played"] = merged["seasons_played"] - pdata["seasons_played"] = merged.get("seasons_played") + #pdata["seasons_played"] = merged.get("seasons_played") # Attach career tags for tag in ( @@ -558,21 +578,12 @@ class DashLeagueGUI: for p in all_players: ms = p.get("match_stats", {}) - # CP - p["CP_Captures"] = ( - p.get("CP_Captures") - or p.get("CP_captures") - or ms.get("CP_Captures") - or ms.get("CP_captures") - or 0 - ) - # DOM p["DOM_Captures"] = ( - p.get("DOM_Captures") - or p.get("DOM_captures") - or ms.get("DOM_Captures") - or ms.get("DOM_captures") + p.get("DOM_Captures") + or p.get("DOM_captures") + or ms.get("DOM_Captures") + or ms.get("DOM_captures") or 0 ) @@ -782,88 +793,82 @@ class DashLeagueGUI: def on_player_spotlight(self): - career_path = os.path.join(BASE_DIR, "career_stats.json") - if not os.path.exists(career_path): - messagebox.showerror("Error", "Build career database first.") + # 1. Check if players are actually selected for a match + if not self.current_match_players: + messagebox.showerror("Error", "Please select teams and click 'Generate Player Slots' first.") return + # 2. Get names ONLY from the 10 players in the current match + names = [p["name"] for p in self.current_match_players] + names.sort() + + # Load the career DB once so the 'show' function can pull match history for trends + career_path = os.path.join(BASE_DIR, "career_stats.json") with open(career_path, "r", encoding="utf-8") as f: db = json.load(f) - names = [p["name"] for p in db["players"].values()] - names.sort() - win = tk.Toplevel(self.root) - win.title("Player Spotlight") - win.geometry("400x500") + win.title("Match Player Spotlight") + win.geometry("450x550") - ttk.Label(win, text="Select Player:").pack(anchor="w", padx=10, pady=5) + ttk.Label(win, text="Select a Player from this Match:").pack(anchor="w", padx=10, pady=5) combo = ttk.Combobox(win, values=names, state="readonly") combo.pack(fill="x", padx=10) - output = tk.Text(win, wrap="word") + output = tk.Text(win, wrap="word", font=("Courier", 10)) output.pack(expand=True, fill="both", padx=10, pady=10) def show(): - name = combo.get() - if not name: + selected_name = combo.get() + if not selected_name: return - player = next((p for p in db["players"].values() if p["name"] == name), None) - if not player: + # Find the match player object (this has the current team/side info) + match_p = next((p for p in self.current_match_players if p["name"] == selected_name), None) + + # Find the full career object (this has the history/matches for the graphics engine) + cid = match_p.get("canonical") + player_data = db["players"].get(cid) + + if not player_data: output.delete("1.0", tk.END) - output.insert(tk.END, "Player not found in career DB.") + output.insert(tk.END, "Error: Player data not found in career database.") return - career = player["career"] - derived = player.get("derived", {}) - obj_pl = player.get("objective_payload_score") - slayer_strength = player.get("slayer_strength", "Unknown") + # Determine Team Side for Colors + # If the player is in the Team B listbox, they are 'Red' + side = "Blue" + team_b_names = [self.team_b_listbox.get(i) for i in range(self.team_b_listbox.size())] + if selected_name in team_b_names: + side = "Red" - consistency = player.get("consistency", {}) - cons_tier = consistency.get("tier", "N/A") - cons_pct = consistency.get("pct") - cons_summary = consistency.get("summary", "No consistency data available.") + # 3. Trigger the 4K Graphic + from utils.graphics_engine import generate_player_card + try: + img_path = generate_player_card(player_data, team_side=side) + self.set_status(f"Generated 4K Spotlight: {selected_name}") + except Exception as e: + print(f"Graphics Error: {e}") - if isinstance(cons_pct, (int, float)): - cons_line = f"{cons_tier} Tier ({cons_pct:.1f} percentile)" - else: - cons_line = f"{cons_tier} Tier" - - text = f"""PLAYER SPOTLIGHT — {name} - -Slayer Tier: {slayer_strength} -Consistency: {cons_line} -Payload Objective Specialist: {obj_pl if obj_pl is not None else "N/A"} - -Career Stats: -KD: {career['KD']:.2f} -Accuracy: {career['accuracy']:.2f} -Kills: {career['kills']} -Deaths: {career['deaths']} -Maps Played: {career['maps']} - -Derived Metrics: -Kills/Map: {derived.get('kills_per_map', 0):.2f} -Deaths/Map: {derived.get('deaths_per_map', 0):.2f} -PushTime/Season: {derived.get('push_time_per_season', 0):.2f} - -Consistency Summary: -{cons_summary} -""" + # 4. Generate the Text Preview for the GUI window + career = player_data["career"] + text = f"PLAYER SPOTLIGHT: {selected_name}\n" + text += f"Team: {match_p.get('team', 'Unknown')}\n" + text += f"-----------------------------------\n\n" + text += f"CAREER STATS:\n" + text += f"Kills: {int(career['kills']):,}\n" + text += f"Maps: {int(career['maps'])}\n" + text += f"KD: {career['KD']:.2f}\n\n" + + text += f"TAG PERCENTILES:\n" + for tag in ["slayer", "sharpshooter", "consistency", "clutch", "anchor"]: + t_obj = player_data.get(tag, {}) + text += f"{tag.title():<14}: {t_obj.get('tier', 'D')} ({t_obj.get('pct', 0):.0f}%)\n" output.delete("1.0", tk.END) output.insert(tk.END, text) - def export(): - content = output.get("1.0", tk.END).strip() - if not content: - messagebox.showerror("Error", "No spotlight text to export.") - return - path = export_to_obs("spotlight.txt", content) - messagebox.showinfo("Exported", f"Spotlight exported to:\n{path}") - ttk.Button(win, text="Show Spotlight", command=show).pack(pady=5) - ttk.Button(win, text="Export to OBS", command=export).pack(pady=5) + ttk.Button(win, text="Generate 4K Graphic", command=show).pack(pady=10) def on_team_identity(self): team = self.team_a_var.get() @@ -932,7 +937,7 @@ Consistency Summary: # Domination Objective Specialist (match-based) # --------------------------------------------------------- if map_type == "Domination": - dom_info = player_entry.get("objective_domination_components", {}) + dom_info = player_entry.get("objective_domination", {}) dom_score = dom_info.get("raw", 0.0) tags_out.append(f"ObjDOM {dom_score:.2f}") @@ -1075,6 +1080,15 @@ Consistency Summary: tag = combo.get() if not tag: return + + from match_engine import load_career_db, attach_career_tags_to_match_player + career_db = load_career_db() + if career_db: + for p in self.current_match_players: + cid = p.get("canonical") + if cid and cid in career_db["players"]: + attach_career_tags_to_match_player(p, career_db["players"][cid]) + team_a = self.team_a_var.get() team_b = self.team_b_var.get() @@ -1284,9 +1298,6 @@ Consistency Summary: text += "- KillsNorm = Kills per map normalized against league average\n" text += "Breaker measures disruptive offensive pressure through high damage output and aggressive fragging.\n" - - - output.delete("1.0", tk.END) output.insert(tk.END, text) @@ -1303,36 +1314,42 @@ Consistency Summary: ttk.Button(win, text="Show Rankings", command=show).pack(pady=5) + # dashleague_cast_tool.py -> on_rookie_leaderboard() + def on_rookie_leaderboard(self): from analysis.rookie_leaderboard import rank_rookies_by_tag - season = self.current_season - tag = "slayer" # default tag for rookie leaderboard + tag = "slayer" rookies = rank_rookies_by_tag(season, tag) - # Build text output lines = [] - lines.append(f"ROOKIE LEADERBOARD — {tag.upper()} — Season {season}") + lines.append(f"ROOKIE LEADERBOARD — Season {season}") + lines.append("Rookies: < 50 maps and < 1000 kills.") lines.append("") - lines.append(f"{'Rank':<5} {'Name':<18} {'Team':<8} {'Tier':<6} {'Pct':<6} {'Raw':<6}") - lines.append("-" * 60) + # Updated Header + lines.append(f"{'Rank':<5} {'Name':<18} {'Team':<12} {'Score':<10} {'KD':<6} {'Maps':<6} {'Kills':<8}") + lines.append("-" * 75) for r in rookies: - pct = f"{r['pct']:.0f}%" if r["pct"] is not None else "-" - raw = f"{r['raw']:.2f}" - lines.append(f"{r['rank']:<5} {r['name']:<18} {r['team']:<8} {r['tier']:<6} {pct:<6} {raw:<6}") + # Format thousands for score (e.g. 96,107) + score_str = f"{int(r['score']):,}" + kd_str = f"{r['kd']:.2f}" + maps_str = f"{int(r['maps'])}" + kills_str = f"{int(r['kills'])}" + + lines.append(f"{r['rank']:<5} {r['name']:<18} {r['team']:<12} {score_str:<10} {kd_str:<6} {maps_str:<6} {kills_str:<8}") text = "\n".join(lines) - # Popup window win = tk.Toplevel(self.root) - win.title("Rookie Leaderboard") - win.geometry("600x500") + win.title("Rookie Standings") + win.geometry("800x550") - output = tk.Text(win, wrap="word") + output = tk.Text(win, wrap="none", font=("Courier", 10)) output.pack(expand=True, fill="both", padx=10, pady=10) output.insert(tk.END, text) + output.config(state="disabled") def export_all_obs_files(self): diff --git a/data/career_db.py b/data/career_db.py index 981336f..75ddba4 100644 --- a/data/career_db.py +++ b/data/career_db.py @@ -57,34 +57,46 @@ def build_career_database(output_path): if not matches: continue - # Infer "season" from earliest CycleID (or whatever you treat as season) + # Infer "season" from earliest CycleID first_cycle = min(m.get("CycleID", 0) for m in matches) or 0 - # Resolve canonical identity AND stamp first_season + # Resolve canonical identity canonical_id = pir.resolve(raw_uuid, season=first_cycle) pir.add_name(canonical_id, name) - - # Use CANONICAL ID as the key in the career DB pid = canonical_id - - #print("DEBUG MATCH ROW FOR", pid, ":", matches[0]) - #break - career_raw = defaultdict(float) + teams_seen = [] + + # LOOKUP CLEAN NAMES FROM REGISTRY + reg_entry = pir.data["canonical"].get(pid, {}) + reg_teams = reg_entry.get("team_history", []) for m in matches: - career_raw["kills"] += m["Kills"] - career_raw["deaths"] += m["Deaths"] - career_raw["damage"] += m["Damage"] - career_raw["shots"] += m["Shots"] - career_raw["shots_hit"] += m["ShotsHit"] - career_raw["headshots"] += m["Headshots"] - career_raw["PAY_PushTime"] += m["PAY_PushTime"] - career_raw["DOM_captures"] += m["DOM_Captures"] - career_raw["DOM_counters"] += m["DOM_Counters"] - career_raw["maps"] += 1 + # 1. Handle Team Names + t_raw = str(m.get("TeamUUID") or m.get("team") or "") + + # Use registry name if available, otherwise raw + clean_team = reg_teams[-1] if reg_teams else t_raw + + if clean_team and (not teams_seen or teams_seen[-1] != clean_team): + # Filter out raw UUIDs (e.g. "e960a65e...") + if not (len(clean_team) > 20 and "-" in clean_team): + teams_seen.append(clean_team) + # 2. Aggregate Stats (Including Score) + career_raw["kills"] += m.get("Kills", 0) + career_raw["deaths"] += m.get("Deaths", 0) + career_raw["damage"] += m.get("Damage", 0) + career_raw["score"] += m.get("Score", 0) + career_raw["shots"] += m.get("Shots", 0) + career_raw["shots_hit"] += m.get("ShotsHit", 0) + career_raw["headshots"] += m.get("Headshots", 0) + career_raw["PAY_PushTime"] += m.get("PAY_PushTime", 0) + career_raw["DOM_Captures"] += m.get("DOM_Captures", 0) + career_raw["DOM_Counters"] += m.get("DOM_Counters", 0) + career_raw["maps"] += 1 + maps = max(1, career_raw["maps"]) KD = career_raw["kills"] / career_raw["deaths"] if career_raw["deaths"] > 0 else career_raw["kills"] accuracy = (career_raw["shots_hit"] / career_raw["shots"]) if career_raw["shots"] > 0 else 0.0 @@ -97,302 +109,117 @@ def build_career_database(output_path): final_db["players"][pid] = { "name": name, + "first_season": pir.get_first_season(canonical_id), + "team_history": teams_seen, "career": { "kills": career_raw["kills"], "deaths": career_raw["deaths"], + "score": career_raw["score"], "KD": KD, "accuracy": accuracy, "push_time": career_raw["PAY_PushTime"], - "DOM_Captures": int(career_raw["DOM_captures"]), - "DOM_Counters": int(career_raw["DOM_counters"]), - "maps": career_raw["maps"], + "DOM_Captures": int(career_raw["DOM_Captures"]), + "DOM_Counters": int(career_raw["DOM_Counters"]), + "maps": int(career_raw["maps"]), "damage": career_raw["damage"], "shots": career_raw["shots"], "shots_hit": career_raw["shots_hit"], "headshots": career_raw["headshots"], }, - "DOM_Captures": int(career_raw["DOM_captures"]), # 👈 add this - "DOM_Counters": int(career_raw["DOM_counters"]), # 👈 and this + "DOM_Captures": int(career_raw["DOM_Captures"]), + "DOM_Counters": int(career_raw["DOM_Counters"]), "derived": derived, "matches": matches, } # --------------------------------------------------------- - # 3. PRECOMPUTE RAW CLUTCH VALUES (required for distribution) + # 3. PRE-COMPUTE RAW VALUES FOR DISTRIBUTIONS # --------------------------------------------------------- all_players = list(final_db["players"].values()) for pdata in all_players: + # A. Clutch Raw pdata["clutch_raw"] = compute_clutch_raw(pdata) - # --------------------------------------------------------- - # NORMALIZE CAREER STAT KEYS BEFORE LEAGUE METRICS - # --------------------------------------------------------- - for pid, pdata in final_db["players"].items(): - career = pdata.get("career", {}) - - career["push_time"] = ( - career.get("push_time") - or career.get("PAY_PushTime") - or pdata.get("PAY_PushTime") - or 0 - ) - - career["captures"] = ( - career.get("captures") - or career.get("DOM_Captures") - or pdata.get("DOM_Captures") - or 0 - ) - - career["counters"] = ( - career.get("counters") - or career.get("DOM_Counters") - or pdata.get("DOM_Counters") - or 0 - ) - - career["damage"] = ( - career.get("damage") - or career.get("Damage") - or pdata.get("Damage") - or 0 - ) - - career["kills"] = ( - career.get("kills") - or career.get("Kills") - or pdata.get("Kills") - or 0 - ) - - career["deaths"] = ( - career.get("deaths") - or career.get("Deaths") - or pdata.get("Deaths") - or 0 - ) - - career["maps"] = ( - career.get("maps") - or pdata.get("maps") - or pdata.get("career", {}).get("maps") - or 1 - ) - - pdata["career"] = career - - + # B. Consistency Raw (Calibration for Top Players) + matches = pdata.get("matches", []) + per_match_scores = [] + for m in matches: + # Calculate a "Performance Score" for every single match played + s_kills = m.get("Kills", 0) + s_deaths = max(1, m.get("Deaths", 0)) + s_dmg = m.get("Damage", 0) + # Simple match power formula + match_perf = (s_kills / 25) * 0.4 + (s_dmg / 8000) * 0.4 + ((s_kills / s_deaths) / 4) * 0.2 + per_match_scores.append(match_perf) + + if len(per_match_scores) > 1: + mean = sum(per_match_scores) / len(per_match_scores) + # Variance calculation + var = sum((x - mean) ** 2 for x in per_match_scores) / len(per_match_scores) + # Use Standard Deviation (sqrt of variance) to avoid punishing high-scorers + pdata["consistency_raw"] = 1 / (1 + (var ** 0.5)) + else: + # Default for players with only 1 match (can't measure consistency yet) + pdata["consistency_raw"] = 0.5 # --------------------------------------------------------- - # 4. Compute league metrics for ALL TAGS + # 4. Compute League Metrics (Distributions now populated) # --------------------------------------------------------- league_averages, distributions = compute_league_metrics(all_players) clutch_averages, clutch_distribution = compute_league_clutch_metrics(all_players) - - # Merge clutch averages into league averages league_averages.update(clutch_averages) - final_db["league_averages"] = league_averages - - - - # --------------------------------------------------------- - # 4.6 Build Breaker distribution - # --------------------------------------------------------- - - breaker_distribution = [] + # --------------------------------------------------------- + # 5. Finalize Tag Calculation (Percentiles & Tiers) + # --------------------------------------------------------- for pid, pdata in final_db["players"].items(): career = pdata["career"] - damage_per_map = career["damage"] / max(1, career["maps"]) - kills_per_map = career["kills"] / max(1, career["maps"]) - raw_breaker = compute_breaker_raw(damage_per_map, kills_per_map) - breaker_distribution.append(raw_breaker) - - - # --------------------------------------------------------- - # 5. Compute all tags using distributions - # --------------------------------------------------------- - for pid, pdata in final_db["players"].items(): - - # ----------------------------------------------------- - # Slayer - # ----------------------------------------------------- - pdata["slayer"] = compute_slayer_for_career_player( - pdata, - league_averages, - distributions["slayer"] - ) - - # ----------------------------------------------------- - # Sharpshooter - # ----------------------------------------------------- - pdata["sharpshooter"] = compute_sharpshooter_for_career_player( - pdata["career"], - league_averages, - distributions["sharpshooter"] - ) - - # ----------------------------------------------------- - # Payload Objective Specialist - # ----------------------------------------------------- - pdata["objective_payload"] = compute_payload_tag( - pdata, - league_averages, - distributions["payload"] - ) - - # ----------------------------------------------------- - # Domination Objective Specialist - # ----------------------------------------------------- - pdata["objective_domination"] = compute_dom_objective_for_career_player( - pdata, - league_averages, - distributions["domination"] - ) - - # --------------------------------------------------------- - # Consistency (Hybrid Career Tag - New System) - # --------------------------------------------------------- - matches = pdata["matches"] - maps_played = pdata["career"].get("maps", 0) - - per_match_scores = [] - for m in matches: - dmg = m.get("Damage", 0) - kills = m.get("Kills", 0) - deaths = m.get("Deaths", 0) - kd = kills / deaths if deaths > 0 else kills - - slayer_component = ( - (dmg / 10000) * 0.40 + - (kills / 30) * 0.40 + - (kd / 5) * 0.20 - ) - - push = m.get("PAY_PushTime", 0) - caps = m.get("DOM_Captures", 0) - counters = m.get("DOM_Counters", 0) - - objective_component = ( - (push / 300) * 0.40 + - (caps / 20) * 0.30 + - (counters / 20) * 0.30 - ) - - shots = m.get("Shots", 0) - shots_hit = m.get("ShotsHit", 0) - accuracy = (shots_hit / shots) if shots > 0 else 0.0 - accuracy_component = accuracy * 0.20 - - perf = slayer_component + objective_component + accuracy_component - per_match_scores.append(perf) - - if per_match_scores: - floor_value = min(per_match_scores) - avg_value = sum(per_match_scores) / len(per_match_scores) - - mean = avg_value - variance = sum((x - mean) ** 2 for x in per_match_scores) / len(per_match_scores) - adjusted_variance = variance * (1 + 1 / max(1, len(per_match_scores))) - stability_value = 1 / (1 + adjusted_variance) - else: - floor_value = 0.0 - avg_value = 0.0 - stability_value = 0.0 - - consistency_result = build_consistency_tag( - floor_current=floor_value, - floor_career=floor_value, - floor_league=league_averages.get("consistency_floor", 0.0), - - stability_current=stability_value, - stability_career=stability_value, - stability_league=league_averages.get("consistency_stability", 0.0), - - average_current=avg_value, - average_career=avg_value, - average_league=league_averages.get("consistency_average", 0.0), - - matches_played_current=maps_played, - percentile=None - ) + # --- Standard Tags --- + pdata["slayer"] = compute_slayer_for_career_player(pdata, league_averages, distributions["slayer"]) + pdata["sharpshooter"] = compute_sharpshooter_for_career_player(career, league_averages, distributions["sharpshooter"]) + pdata["objective_payload"] = compute_payload_tag(pdata, league_averages, distributions["payload"]) + pdata["objective_domination"] = compute_dom_objective_for_career_player(pdata, league_averages, distributions["domination"]) + # --- Consistency Tag (Fixed logic) --- + raw_cons = pdata.get("consistency_raw", 0.5) + cons_pct = percentile_rank(raw_cons, distributions.get("consistency", [])) + cons_tier = tier_from_percentile(cons_pct) pdata["consistency"] = { - "raw": consistency_result.normalized_score, - "pct": None, - "tier": consistency_result.tier, - "summary": consistency_result.summary_short, - "components": { - "floor": consistency_result.components.floor, - "stability": consistency_result.components.stability, - "average": consistency_result.components.average_performance, - }, - "extras": consistency_result.extras, + "raw": raw_cons, + "pct": cons_pct, + "tier": cons_tier, + "summary": f"{cons_tier} Tier Stability ({cons_pct:.1f} percentile)" } + + # --- Advanced Tags --- + pdata["anchor"] = compute_anchor_for_career_player(pdata, league_averages, distributions["anchor"], raw_cons) + pdata["clutch"] = compute_clutch(pdata, league_averages, clutch_distribution) + + # --- Breaker Tag --- + dmg_map = career["damage"] / max(1, career["maps"]) + kills_map = career["kills"] / max(1, career["maps"]) + raw_breaker = compute_breaker_raw(dmg_map, kills_map) + + # We handle breaker distribution locally for simplicity + breaker_dist = sorted([ (p["career"]["damage"]/max(1,p["career"]["maps"])*0.6 + p["career"]["kills"]/max(1,p["career"]["maps"])*0.4) for p in all_players]) + break_pct = percentile_rank(raw_breaker, breaker_dist) + break_tier = tier_from_percentile(break_pct) - # --------------------------------------------------------- - # Career Consistency Percentile + Tier (NEW) - # --------------------------------------------------------- - raw_cons = pdata["consistency"]["raw"] - cons_dist = distributions.get("consistency", []) - - pct = percentile_rank(raw_cons, cons_dist) - tier = tier_from_percentile(pct) - - pdata["consistency"]["pct"] = pct - pdata["consistency"]["tier"] = tier - pdata["consistency"]["summary"] = ( - f"{tier} Tier Consistency ({pct:.1f} percentile)" - ) - - # --------------------------------------------------------- - # Anchor (requires consistency) - # --------------------------------------------------------- - consistency_norm = pdata["consistency"]["raw"] - pdata["anchor"] = compute_anchor_for_career_player( - pdata, - league_averages, - distributions["anchor"], - consistency_norm - ) - - # --------------------------------------------------------- - # Clutch - # --------------------------------------------------------- - pdata["clutch"] = compute_clutch( - pdata, - league_averages, - clutch_distribution - ) - - # --------------------------------------------------------- - # Breaker - # --------------------------------------------------------- - career = pdata["career"] - damage_per_map = career["damage"] / max(1, career["maps"]) - kills_per_map = career["kills"] / max(1, career["maps"]) - - raw_breaker = compute_breaker_raw(damage_per_map, kills_per_map) - pct = percentile_rank(raw_breaker, breaker_distribution) - tier = tier_from_percentile(pct) - pdata["breaker"] = { "raw": raw_breaker, - "pct": pct, - "tier": tier, - "summary": f"Offensive disruptor ({tier} Tier, {pct:.1f} percentile)" + "pct": break_pct, + "tier": break_tier, + "summary": f"{break_tier} Tier Offensive Disruptor" } - - # Save PIR with first_season stamped for all canonical IDs - pir.save() # --------------------------------------------------------- - # 6. Save DB + # 6. Save and Finish # --------------------------------------------------------- + pir.save() with open(output_path, "w", encoding="utf-8") as f: json.dump(final_db, f, indent=2) diff --git a/data/league_metrics.py b/data/league_metrics.py index 3ca340a..e53a4b9 100644 --- a/data/league_metrics.py +++ b/data/league_metrics.py @@ -116,6 +116,9 @@ def compute_league_metrics(all_players): # Domination dist["domination"].append(compute_dom_raw(p)) + # Collect the raw consistency score we will calculate in the next step + dist["consistency"].append(p.get("consistency_raw", 0.0)) + from tags.anchor import compute_anchor_raw # Consistency raw score for distribution building diff --git a/match_engine.py b/match_engine.py index 1c859ad..5087514 100644 --- a/match_engine.py +++ b/match_engine.py @@ -176,13 +176,13 @@ def rank_match_players_sharpshooter(match_players): def rank_match_players_objpl(match_players): return _add_score_fields(rank_match_players_by_tag(match_players, "objective_payload")) - def rank_match_players_objdom(match_players): ranked = [] for p in match_players: - comp = p.get("objective_domination_components", {}) - raw = comp.get("raw", 0.0) + # CHANGE: Look for "objective_domination" instead of "_components" + dom_data = p.get("objective_domination", {}) + raw = dom_data.get("raw", 0.0) ranked.append({ "id": p.get("id"), diff --git a/tags/objective_domination.py b/tags/objective_domination.py index 80456b2..30b24a8 100644 --- a/tags/objective_domination.py +++ b/tags/objective_domination.py @@ -22,49 +22,27 @@ def dom_summary(tier, pct): # --------------------------------------------------------- def compute_dom_raw(pdata): - """ - Computes a normalized 0–1 Domination Objective Specialist score. - Uses ONLY Domination stats: - - captures - - counters - """ + # Try career totals first + caps = pdata.get("career", {}).get("DOM_Captures") or pdata.get("DOM_Captures") or 0 + counters = pdata.get("career", {}).get("DOM_Counters") or pdata.get("DOM_Counters") or 0 + maps = max(1, pdata.get("career", {}).get("maps") or 1) - matches = pdata.get("matches", []) - if not matches: - return 0.05 # minimum floor + # If no career totals, try matches + if caps == 0 and counters == 0: + matches = pdata.get("matches", []) + for m in matches: + caps += m.get("DOM_Captures", 0) or m.get("DOM_captures", 0) + counters += m.get("DOM_Counters", 0) or m.get("DOM_counters", 0) + + avg_caps = caps / maps + avg_counters = counters / maps - dom_matches = 0 - total_caps = 0 - total_counters = 0 + # Use a lower log base to make small numbers more visible to the score + cap_rate = math.log1p(avg_caps) / math.log1p(10.0) + counter_rate = math.log1p(avg_counters) / math.log1p(10.0) - for m in matches: - caps = m.get("DOM_Captures", 0) or 0 - counters = m.get("DOM_Counters", 0) or 0 - - # Skip matches with no DOM stats - if caps == 0 and counters == 0: - continue - - dom_matches += 1 - total_caps += caps - total_counters += counters - - if dom_matches == 0: - return 0.05 # minimum floor - - avg_caps = total_caps / dom_matches - avg_counters = total_counters / dom_matches - - # Log-scaled normalization (smooth extremes, expand mid-range) - cap_rate = math.log1p(avg_caps) / math.log1p(15.0) - counter_rate = math.log1p(avg_counters) / math.log1p(15.0) - - # Counters weighted more heavily (deny enemy scoring) raw = (0.40 * cap_rate) + (0.60 * counter_rate) - - # Soft clamp - raw = max(0.05, min(raw, 1.0)) - return raw + return max(0.05, min(raw, 1.0)) # --------------------------------------------------------- diff --git a/utils/graphics_engine.py b/utils/graphics_engine.py new file mode 100644 index 0000000..17722ad --- /dev/null +++ b/utils/graphics_engine.py @@ -0,0 +1,133 @@ +import os +import math +from PIL import Image, ImageDraw, ImageFont + +# --- PATHING CONFIGURATION --- +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +ASSETS_DIR = os.path.join(BASE_DIR, "assets") +# This points to your stats folder for OBS to read +OUTPUT_PATH = os.path.join(BASE_DIR, "..", "stats", "obs_exports", "spotlight_card.png") + +# Ensure directories exist +os.makedirs(ASSETS_DIR, exist_ok=True) +os.makedirs(os.path.dirname(OUTPUT_PATH), exist_ok=True) + +def generate_player_card(player_data, team_side="Blue"): + """ + Generates a 4K (3840x2160) Player Spotlight card. + team_side: "Blue" or "Red" (determines accent colors) + """ + # 1. Setup Canvas (4K) + width, height = 3840, 2160 + # Professional Blue/Red Hex codes + accent_color = (0, 162, 255) if team_side == "Blue" else (255, 38, 0) + + # 2. Background Logic + bg_path = os.path.join(ASSETS_DIR, "background.png") + if os.path.exists(bg_path): + img = Image.open(bg_path).resize((width, height)) + else: + # Create a deep dark slate gradient-style background + img = Image.new('RGB', (width, height), color=(10, 10, 15)) + + draw = ImageDraw.Draw(img, 'RGBA') # RGBA for semi-transparent lines + + # Font Helper + def get_font(size): + # On Windows, arial is standard. On Linux/Mac, you might need a path to a .ttf + try: + return ImageFont.truetype("arial.ttf", size) + except: + return ImageFont.load_default() + + # Define Font Styles + title_font = get_font(220) + label_font = get_font(70) + stat_font = get_font(130) + tag_font = get_font(90) + hook_font = get_font(60) + + # 3. DRAW PLAYER HEADER + name = player_data.get("name", "Unknown Player").upper() + draw.text((200, 150), name, font=title_font, fill=(255, 255, 255)) + + # 4. TREND ANALYSIS (Last 5 Matches vs Career) + matches = player_data.get("matches", []) + career_kd = player_data.get("career", {}).get("KD", 1.0) + + recent_kd = career_kd + if len(matches) >= 3: + last_5 = matches[-5:] + recent_kills = sum(m.get("Kills", 0) or 0 for m in last_5) + recent_deaths = sum(m.get("Deaths", 0) or 0 for m in last_5) + recent_kd = recent_kills / recent_deaths if recent_deaths > 0 else recent_kills + + diff = recent_kd - career_kd + trend_color = (50, 255, 50) if diff >= 0 else (255, 50, 50) + trend_text = "HOT FORM" if diff >= 0 else "COOLING" + + # Draw Trend Box + draw.rounded_rectangle([200, 400, 850, 500], radius=20, fill=trend_color) + draw.text((230, 415), f"{trend_text} ({'+' if diff >=0 else ''}{diff:.2f} KD)", font=get_font(45), fill=(0,0,0)) + + # 5. LEFT COLUMN: CORE CAREER STATS + career = player_data.get("career", {}) + stats = [ + ("TOTAL KILLS", f"{int(career.get('kills', 0)):,}"), + ("TOTAL SCORE", f"{int(career.get('score', 0)):,}"), + ("MAPS PLAYED", f"{int(career.get('maps', 0))}"), + ("CAREER KD", f"{career.get('KD', 0):.2f}") + ] + + curr_y = 650 + for label, value in stats: + draw.text((200, curr_y), label, font=label_font, fill=(180, 180, 180)) + draw.text((200, curr_y + 80), value, font=stat_font, fill=(255, 255, 255)) + curr_y += 280 + + # 6. RIGHT COLUMN: SKILL BARS + draw.text((2000, 450), "SKILL PROFILE vs LEAGUE AVG", font=label_font, fill=accent_color) + + tags_to_show = [ + ("SLAYING", "slayer"), + ("MARKSMAN", "sharpshooter"), + ("CONSISTENCY", "consistency"), + ("PRESSURE", "clutch"), + ("DEFENSIVE", "anchor") + ] + + bar_y = 550 + for display_name, key in tags_to_show: + tag_obj = player_data.get(key, {}) + pct = tag_obj.get("pct", 0) + tier = tag_obj.get("tier", "D") + + # Label + draw.text((2000, bar_y), f"{display_name} ({tier})", font=tag_font, fill=(255, 255, 255)) + + # Bar BG + draw.rectangle([2000, bar_y + 110, 3600, bar_y + 140], fill=(40, 40, 45)) + # Actual Data Bar + fill_width = 2000 + (1600 * (pct / 100)) + draw.rectangle([2000, bar_y + 110, fill_width, bar_y + 140], fill=accent_color) + + # --- LEAGUE AVG LINE (50%) --- + avg_x = 2000 + (1600 * 0.5) + # Drawing a vertical white line at 50% + draw.line([avg_x, bar_y + 100, avg_x, bar_y + 150], fill=(255, 255, 255, 180), width=6) + + bar_y += 230 + + # 7. FOOTER: SCOUTING REPORT + # Get the summary from their highest-performing tag + best_tag_key = max(tags_to_show, key=lambda x: player_data.get(x[1], {}).get("pct", 0))[1] + intel = player_data.get(best_tag_key, {}).get("summary", "Reliable league veteran.") + + # Semi-transparent dark box for footer + draw.rectangle([150, 1800, 3690, 2050], fill=(20, 20, 25)) + draw.text((200, 1830), "SCOUTING REPORT", font=label_font, fill=accent_color) + draw.text((200, 1920), intel, font=hook_font, fill=(230, 230, 230)) + + # 8. SAVE + img.save(OUTPUT_PATH) + return OUTPUT_PATH \ No newline at end of file