From 3e5f141938a6c496a299f7fc4d3afb0518eb39f3 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 5 Nov 2023 23:06:28 +0000 Subject: [PATCH] feat(overlay): add leaderboard toggle for BL and SS --- next.config.js | 6 + public/assets/logos/beatleader.png | Bin 0 -> 24531 bytes public/assets/logos/scoresaber.png | Bin 0 -> 1572 bytes src/app/overlay/builder/page.tsx | 8 +- src/app/overlay/page.tsx | 52 ++++++-- src/components/overlay/PlayerStats.tsx | 22 ++- src/overlay/type/overlayPlayer.ts | 8 ++ src/schemas/beatleader/difficulty.ts | 30 +++++ src/schemas/beatleader/leaderboard.ts | 16 +++ src/schemas/beatleader/metadata.ts | 5 + src/schemas/beatleader/modifierRating.ts | 18 +++ src/schemas/beatleader/modifiers.ts | 16 +++ src/schemas/beatleader/player.ts | 9 ++ src/schemas/beatleader/score.ts | 51 +++++++ src/schemas/beatleader/scoreImprovement.ts | 19 +++ src/schemas/beatleader/scoreOffsets.ts | 8 ++ src/schemas/beatleader/scores.ts | 0 .../beatleader/smaller/smallerLeaderboard.ts | 5 + .../beatleader/smaller/smallerScore.ts | 14 ++ .../smaller/smallerScoreImprovement.ts | 9 ++ src/schemas/beatleader/smaller/smallerSong.ts | 4 + src/schemas/beatleader/song.ts | 16 +++ src/utils/beatleader/api.ts | 126 ++++++++++++++++++ 23 files changed, 424 insertions(+), 18 deletions(-) create mode 100644 public/assets/logos/beatleader.png create mode 100644 public/assets/logos/scoresaber.png create mode 100644 src/overlay/type/overlayPlayer.ts create mode 100644 src/schemas/beatleader/difficulty.ts create mode 100644 src/schemas/beatleader/leaderboard.ts create mode 100644 src/schemas/beatleader/metadata.ts create mode 100644 src/schemas/beatleader/modifierRating.ts create mode 100644 src/schemas/beatleader/modifiers.ts create mode 100644 src/schemas/beatleader/player.ts create mode 100644 src/schemas/beatleader/score.ts create mode 100644 src/schemas/beatleader/scoreImprovement.ts create mode 100644 src/schemas/beatleader/scoreOffsets.ts create mode 100644 src/schemas/beatleader/scores.ts create mode 100644 src/schemas/beatleader/smaller/smallerLeaderboard.ts create mode 100644 src/schemas/beatleader/smaller/smallerScore.ts create mode 100644 src/schemas/beatleader/smaller/smallerScoreImprovement.ts create mode 100644 src/schemas/beatleader/smaller/smallerSong.ts create mode 100644 src/schemas/beatleader/song.ts create mode 100644 src/utils/beatleader/api.ts diff --git a/next.config.js b/next.config.js index bca61d9..776a365 100644 --- a/next.config.js +++ b/next.config.js @@ -43,6 +43,12 @@ const nextConfig = { port: "", pathname: "/**", }, + { + protocol: "https", + hostname: "avatars.akamai.steamstatic.com", + port: "", + pathname: "/**", + }, ], }, }; diff --git a/public/assets/logos/beatleader.png b/public/assets/logos/beatleader.png new file mode 100644 index 0000000000000000000000000000000000000000..9cf241b45e0b00beeb541fd778da8bcc731b6d33 GIT binary patch literal 24531 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 kxMB#%T}qb2 zddYo5f?d2zNY&7(wb;xX6ed-XmB-}fX$7!A{g*D*xrnX5zKAoGzSs8DWzRiDOOIRW z-2axDzrT|1@wlLxqpc0A=fLIpOfg1fU9Xv7TmmJNY0WVS7-S*NAvau^$Nsbxdl=(k zszqIEOHGs10JLvC>$Jar%;AP5fEkJ&9SmN$s1{gqkK*qePAEiVNlap&HWwCo z1A;AyojgwYez?rQ+yJWAq@?Kt!>`c5zroA+5g=l^72d*=bQ*r`_s9X5rar%tm)jcUazLBq4b=W5~)@dhjD?l zi~%*QR+q^n_p$MOEWrtL+n`wgo{+vh0?ktk%mC@BXmxr0sdMHc-X{O)Kc0DQOM`af zc7(DmYnw}80JAlXuoR4y-LBSSw!W818(oH0^6;Yom}z<+|D{^5QWzU+chCCMmuzEZ z$6Viik@&)t*>llz(lhP>-V@7zAR`S#HyXC5r!IQ#^QIi`}%)$DWxp! zgeVZPb@&sbXcxd~wObcst1ocs)Uk2{Hfd;Qiurr_NBfy(jmU1z^*QR0&_4crRFs&|aH^CsY>1m-Z-bWH)eU^=BV zj%l^{sAR`O6p`w3eNJb4v5mkJFr@}n2RQATE=h&?zP{6mjx zE|uP;nzt!#TXO8&wTo@8?G6K-RouvB`ab=0N1W9x@RkJ{RWle{8x)ufOEjh1a?vA};{IXUwiS2ZQ~4GH59vrSsyk z=@K2ei)sBa)iQUrP@X2uQ@XCo-4m&Pv}=mIxW;v!G!=lc(;}FGo{LAq&`R6i|*@beJ$(XtX96#gjs70l1&o^Z<#lN3{Jw0?ZbISGF z{-tD1C;NL5GHr?)ZtnTP95eNPG7OR0dP$C%DqT_unk<)Tb$@cQkHQTjw$2W^6iKh* zfPEg5xnn{a#4POt$roj0sF+TVat%WV3*s=mFhF@Z*a4<9UN4ylE^Cgt;aWIgg)q0G z55}2SxpJpcDbHTBe{0%c>R3HqM9vnsq3Fp=U<@i+WBaPNJwmbqgRSu^QU z(>Ib)uT|c~6nKUi4N*Jh$r4_gkoauZ%kU7W z-I_7FfWg`EsMYRQjPE65KGo6^mMrNUL_7h2S79bLa1rxnDsiuNPR;yU0@{neKjX|k z^Y=v~)!8{~kEOmStvB6byxx$4BJ0mF7p{{sUL#oXBj<)lb$CR35rSL^kcXp8*X zh``K0DIy=7I|gPzG+e;!*%Ni%yT3x8Afhg;KT2$-I@0hmFvQx-zJ$uC`|2tWP0Dx7 zY4Uo``mM(edR#J*>ZznPOZy_)v82DLq`rOEnY*@Y`vN!ftee%o z6Ek-Ga?GU}lSr`Uzh8|AtQuU45|GB&qd^+s5#tjYs7qR8Jt_tW!Jdyk_3>-#e>1vY zmkj(Vf0t~Vx=u~|ObsNe3|ew+{(<6oN@3sEfn33vNyF--wrYv$CLdY_49 zuLA{;HX7oGi0Jj=)Y=f)eM4x|-=?}0gpt>eJP!Z6u{ORJ|@WS^5QyRkr zu~zO(alDShJm=~DWADsbbXk@ABn!O_e|u4n4>%MVS?AuBYc1^smo9%B0Ddw(=L|zv z-%Dlx5|7c;*y-B-;{1Pth<~&4E~eg1ZGE)OW&*5fG6@VYb8y9skAam#yVlEeDH0*2-7$TP0uwnrAL&bf9tH&Y&O=Y%TL@;XL(=ps%7m>dZdYT$YbWKZ3*xXyjoMx0LQtHoj#C;kPK z=~4o^!zh_?f0;A}79H#=_mxXDnZhk#RF^4tx2_!!3}nmDO!b0ufVw;n%4>x3kl!OR z%!$1uhEN`7sfB`1@SKx3eB4JK{dhBNWO(UP5YU z@|;mh|6UW^rd})fTXPgKED>B^#+d4vGBmZQ>qYaNoTm7G0Qd$G`LDA!%(zO+eRC1# zCC5(e`;!3heG~1r&r?44+`Gz>9*xN~arsB52&R)z1R=--0aphSL&p}=ztqj zIOU0|zC&D5yF)aM5XVvK7?{h1VLf17KF4aVvmxR)%pMf?=pd^g06Dl`JyOlZFw&&} zx?C^MukB|d+9KkA0s#L_NVBfe62Luo&dhP{o_{O|LwA8^So-(Ucd;5pydZJ0yb^_B zT?b{R=`LY_5T-fB8;Lb0ye&A{}&kG9nHICo9&ibQlK)ZY$3`+o~Pw{O#edFL)dP7^s32@$`6i2mjt zbIUUHlnJ?nvUd|phQcNAPR&D~yiMzeufjbCzFA+=0SrDMHJ9Qq#9txfZCdq(PMJ$n zu?1KdgP6Ley{9vw?hw)Uh{&%Mk?(hcQMy_$A{@oBP7wY-0QhDi`hAG_=ALIL*DrEx z&GBG~*3M;$7(x(D01k}_W!H`n9OnM7s#Vdff8 z^S&9cU`smwg4DhVa7#V0$s|OYLv#{ghEqTU@hyIOeR5VU+iI@?|tsLrG9%| z4~+Zs0PshGi}phz@@eL00C-VEo&kWPfJ4pz;65UL4gg*eksk+u9|eG)F(cmQ z?m4%wCcsOsQJxbgZBOT{ry*cqc(C53iic@kd7$__xOy2HP1rXoZW`S64kKDg6m(T&9&b-4U%n-X6f45u3{O#FqZ~=0mc=(I=ScJ)j@S> z9-)+4f>cZ79;e5nq7uZuw1O$d+Q-m~bA2vwPOS|~&RHUSwRTKhyLP=voMF8(tC`yg z8(;>otr<0#PU{8cjJHgGFHb`RVX)~MNhom87AvdZ(#EZs4dwch{Zk;dJZ|p#bJt!P zKiSvD8n~Vl)O{w?)OE;uX4VjjGUkxSAqow#qJk)`luIaO8H7M6X#>c%4T6Cyr*2qq z6ll)Wxd;HCI{qTtmtK1*$Sv3Bn7KT@wrvTvh_zkQIjg1qU=g7%HN@q4nbcIewDfP) z5b9+bG)2-9h@C>3Lko|oX^h%=`|f|yXISDUo-%``fH;)b zUOSj-sfr;G2QVE>DPA|pA$6jc0;&h>P{bRg5D;1Ksfuy2Q$7Ej9bGeT=FA>gz^HTA zUfL#}VAzY`l_k$GwO%#h%52wFOeTaZ7#B4{WPT3XfuBSiVy%Z#5r`3K>s_)q0`pAQ zwJP^j+P~D~nWKxCG8dte;pXoBIBlrCgVHpD88fMN&ZPwAtV?wO(KY;lYkQi=YKiqh zHJG3rTFo#7>azT%71i%dq#720>e?|dVUCpB_5ss<+GgsQeE@t8eAn7Lr)lQ2-`x5t zG-7ybDpiDHMYgecX#MKnHuf+wk!%b?qHsJu*+_LUgcJ(1BhnJ{V#SqX3~`DG`f#5m$C@&V^hZT)83^) zd#pp&!*mItI;L)bF^8xs2q^2Oe_O3kXo&KI!@?eXj={_8!i-5Wckl8(OYS}Q3|#zO z6S!I%%Ii*ZcFx>0OdY!fE6oXeRa41$Uhu~v8M$32ViM$+ z6?un(xunl|Np4r0$Ok!YKr;elB@JzCh zgkwJ=Rs=%!3x^oy&$NO4MLURSeM+S)*Sgr?Rt-sgZoyhAIQe|0B?2MCul7n1vVx0 zpN&5N8OaJk{9d->oAOMRz}bRpj~~!zh)WR)ZP)rG38fhZ0-0>rG>la8Y}etu;=|Qo zM)?lfL10usoSZ$K(v!0X-NR7(j+^T^=9m~}K=YI^mHU?n+FaXULfC4@&E6+6Gu}eh z(Hrp<_W;-y9|z;lGRBqCB&wN96o56YD-0o1+PhuXi&Eo|9wlJht&k$r8PFDsG15gc zjWD$xZ8~Hd*v<7;rvUJ^z;I`HQ0pd~pg9-nMb6o`Z|~rJEcm1K@%`iN1Yc0XEt?0a1H=p2PD^r+@={#F+~?OpQf(4WZRqxRXbO+u=C%GhHIpEU-Y*|Ymn_TGfiNAqh~2D zysnptZCl&*O{Qq(56Eh2eZfOfD)1CPmV~0gjL<>nJ_f@P5I#hftpL0O0N(~ApAg13 z{q^GqonojNExFXps_g?9#%D~OGqtU3A1xK^aK9zymTlkEbCAgbApGUX@Qpxl9{{%q ziX(0U^xK|9es6^`gM$NBq6GBW_^EHF2vA!-l*2o&CJ8dUZ;)ACl->tHfv zMQLR)snWKxrx<<{^9y=kcP!{jPO}h_zF0=?HXWSr>lAH|y&x;^|L1kZ*kWztT2$G+(e?>!Ju2%Kqh>+c$QjP^!ZB;@dAbtnlfy3>gx?JUs zFg^&j?Fle!faG}re0OL|BiW~@F;4a%7pKxmG1rSvUSq*A*|jW92}jB7d+*VE(bN(- zJ$lUW0GydbawgvG`ZN%qF`aBqpy?aJCPaKa#-q@o`Fl!o&r%W83!NWi9;IpgKzlU= zPVm(pss!fdt(eS*U44-f)4#!J0jOw%7+IlY294A^rW674JqS}HjnfvojT&Ow5p{0rvDASX*ugD3bvzC31=pl$ zD2Pyd7V2`5SV=td}F!JUXEq#7@XJ!Dk1 z(IlbRF4YgL8UmWGf$g;|+&R4iH~X8=95f8q!28KN;Mmha2bnvzk_=c|pR%X(mt1V0 zy58KjiT$MudGbCB?@t$LhtdW>S^+o)Qf?vD5^#QUPWSOX$dOQU-nb5mRFjaJxYSFZ z?p1U?$t<=b8)Z%ytcFM?(=rAn%uIsOn<(uUpY7VFJRrItlo&@bQ$%|FGSHY&QnWBI zU)GcO$RSZww^2)g_coC}?9NMHchtd``!B=6_5cpg4&ksl)MHdYE+?@0ONN96n3?*s z9?tn3&YN@SHXSwCKy%g*2`ORYwKuTW=n-5?=3?|q1oKIQ76G`P#zN4iePq1w$)?ut zJXZyAc%bl0$t#xl*kcG9a(~>zPhO;QpKPWZumgas{T3fOYKClkGK-AI!}cO>y958s zz^rBysdjN6LVGaRGw~?{w+#KJhllhK9`FM%P)v&f@)cDmxKi2mD{aV6W`s?*QH`LQ zfr1$;q`$$dm%w!VrpEb@b$+@XVZaW-{n}?>nlg z0ToU_X#%jMx&`ri83-?LK-L^e^TWmUxZUCB@OQg|9chyWh2;?O+{<<9w}Y;2x<-XUSXdhUpVhPBv|S7inSfr%4ZYG77a;{{es#!H z)*g14L8t-G_=Lngw@w(;oiF5yn&~rwrS|0hL2~H2MJyZj9(mC<* z=B=ovcf8d)LpF47cAf0H`|eUOzq(hAsifN*95loLhItg|qXAPy!B8HE9bzd=G-!&} zI$oWS+o0I+UZzbXq9=zox@Zg@%|oTDXvB z5X5KU3;LyhY18n*C}tL?R2cEmf5%@DFh?u^qQPD@pL*z%Fa%AJ%qEKwb#crW1faKq zK@m#~=F&`ZpMGPMAwW8sj{^hqTg}YBQi3ENZ?t}HFY>C>3q-^= z>~EP~LIh}C0CHXxN&@@o1tlTCqD}L`$193tzklEd9L6&H55lSF00luHrxikP8=3wf z2*5$9DGbOa4a^0cFxCzTedCuwJB)gM4x2cc%BXJpJU%nvAkW9~ynFtfRtMFX9yHBO zAmHZ_BiHd7^GT4cIW-aw88AccQ^M^1hP8wpOkGwd1P9?kIN}p46e5ZvIHr~OGUjA` z(Tte&YU2-lMblKiE7OVSpr64ur(i}|S8E&0r#5K$Bn(bCf&fIV(I#^l2h(V*9Z0o9 zia&VEBGV3jBxdTAnUOtzkO={ZR{_=~o2;R5wKmihqxjGbH)_=l??%QiDf(;%se~Yz z91+cu1S?bRAOqAf<`9H}qELhv&7n8qpb63hfGIs=2+Ry1>IzUl8s(M(aa6)qQ{=Iv zHun7C+Z`0bG4eR685P{F7AN!uP$%u6%q%ez!V1OMqhvKelnWGYof10;)`K5TkP<4@ z%Y=m52}>XHI#7nZ74+e62Ws8L$aMz677LN*tRaK^3}J~6&A^>We#?b2WasYvz*lr}LLlS0NhXs?$rwCN zK@cpsF3qOUA#M_eXlO8#R7eKxAX!(d#6}1bFBq%AcdXVEF9NNHs0_YHz3cpk)D3(G zH7#0gshtxTZ!!h165GKOu>qzRk2i;*Q3cbF!y3L}`X3(*LG+|P*+er_4-})FhWXn< zJEeB4IWP1T735!SN>K-ZtOK)PIeL~1Txgvsf>7e%-=v(iNN zb>#by39UTc%G(0?0(v0L00-?sj=&*vxhZgtn!z;2uOD1L18g6l?4Gf&FN!Rd?u*^| zZGQcyK1lER=qol2H)vx|=jD6j_N0!R3$a&xf7&GF~pyn7~p zg;Uv80Q}j1{9Ewavv=UuFZ+@mifM*qx@}QQuocQmU}nEI*keu24YPclbEb}Yl>2=Z zQCVp{C1pl4*9Zbx?H{T+(Wjq2d)ME3;r@3f^QejDQUKXHd^pHNFomvf@%5*6xV4?~ z2v7rfJ`LTdgzi;?at>H`9y2>})uG2BeaT@6CnXyG)q&y-LbGk0G^y7~djZlkYITLi zyP2r+kbvie;`o_Y`23v@FMg%GyVn2Q4}Ld3|AD{Xy!6pe;^EUL0a&R_@3G7SV?df2>D2mwa{aHf9h#J#7|^QVIWjqql-8GK?*aKJpUUZ#6t zd~5@(wiE$u`VE}5XDpo$fCa4p$yD)yFKae+K@E2F{QG}G?tbpy9vE~kRW|JB{)oT$ zv)>_K{Mqk-=I}lcZN~w>bV}WW^c5nL$UD8?93%FRB4s~Tqmy(jWWFR!xAR2j#NdD=CbWV>p7!1n5K-Fu73t#@Ht*Q8%MZ}H%r4cyqZ1a3*k*`=Xj z!YTWT1>s+Nz5VgnK}Pp`W;(g%axn2Z0BZJt`T!67a9D2@X_Xf2(0G~75a7#dPpNj0 zoxnA^2B&bUxj{svb$8swNyyu0sK8cD8RH(+b$b5&ul1k#@t?=t$@?-+$OuR4X-j{8 z1NyJ-UbB=(n;BgqC3{SM4a@}s2e-6L2|+y0&expH@Q-G&epx`cn18`BH-}1iJ{ipC z=U`ZukZujAH5<~H1PsY{@i+D62)O`AeQ5=DO_NNb3`jRdc+4=-!Qd9Sxy7IVH$Q*y znZSF|72gGiOs5^V2)EE`M;@&g9pKaYE03%^2CmJa=gV0bSvYh_bY6U6ltEPcE;{iMo z5Hsb`W-eT*o&B{gR`P<-MIfEMFHs!f8u+bmw1166wL>@34nEyz2-tei?{z~fe9#?u z&(e7?mkw1Kq~Q$S>~i#FAUr@1bI!bA9QKF6EeA#8r7pR$5V(j#BuRBk`?bIBAK}5Z zS1{aY_(hW%gT*$?^6tjRV3t->--qVhedpJXE4M**-Jzcf+cAGOD-!8h%y3692smQ; zZ01eF_=9G^8O<=B)SL?j&l%eYP-RemR%4aQC74QRgJ-rH1|<)X^%&`S4HhQRSQw&~ zVa&CIzFic!b&jw6o>vZk+%SF84kXk?jS#Xa2J9fB4xuAF@-LnNaD(2%^j2Q&#gBb> zBRI#J;G*gPj_FwA5eiB;^k4g}6h(Hv5qG;Vlj$;7{-xdc7%VSC6a z?slMjuNmC7!yBaI3&5&Eyq4h2X2VU_FmE{7Vi0__K}|)qRnrpN=K1%3hMs@lpR)!A zRMur=Y}jM4!oaoTrnc4YTW+geN3ymJy}4w90He9>21d;UAu+r;)&l5E7z#!oZJ*W* z0yqNFC7$2{L`zxS!U#Y=V*WfWI|@LbX$CYggfURg@j_`Td9RZ1)x&%88=R^1=f2}} zo6khEJYt4~BfMGPCG8NfgnQ@C(mCpmoK{fYRvte9z%PyJL~)8lOI}tK$a4 z_{|Vpi_G3Nm^48bz<4y3I%6s!Vg{r?e5d_O|HV%o|Dfw=-C}u+d}f5l4idCO6oxQp z%n8Lcx~4A_d+qdD>j!*cR0xhzDR6n+myXn;ROkm_AGgQuwk+fjbn@pq73DPp`cZ4{ zZ!4{#!CL{?Za(x~e*tg4=ljOsRVuoo2Ey&Wx&8J!H2|IKswt2T1z^t1m305&>R1EL zkR1tyi8NI7$HLv33j>VIp|LOwrp~-D)U?D{5XkJKv6d(z$y2ytEF95HGMFKmOwFJj zz~|BF`xJnWyw!f+fA@)Ne*s03sT5qUp4a^N95vaKc90+tP8wR~cH8piV8d_%EC=F? zuiX*WDvzE_EF)3}44(;-bEN)RrbFWEyX(+5J#)tzm}B5hNSv1Ff)u*nu7RdA60D2< zjkw)>{dfMw=7kUaPl(L}%%Jb%2DS1vYs!=W8?q{bFzXIRw1625ye>7EomC%=i8O#@ zFmBT-UdY_4g=Ux$0$aZ*6O0*Qm@5@2g%_#MiiQ>1K?nR(#cV2YL-HyO+F=j|%8NG9v}SX&u;$I_x{ASzeplyk*{1=&A__ughTn!cg&Fo6UObOS1^48Z}{(Us-7i= zV*q>wGtCg;LmH~7(Z$aK;d_Ifb>`;)xOQ-jPn>AA?CTzR8D6MBVYuR8BB9qRP;;$X4N~_mjd4N_pf4&61764rgY^Y=$b0{~ znbbw5H`{G+u3WrV2@+;-J^o7%@yCAOr;h%QkG|D@DRi$&i zwCA3I512RohBt>Bh6likAr8Wbj4${v6)xxL9p$43lr z0^`#NazhyJAj>Bty33~pLO=S;0{BrRcpE_O3&B%9$LUReQ}dnLUE5MaTB%Kig0bDX zSAf`72A10qQwO8*w%Z5S_>G_WRr1c4J|O20UxI#n8@PAqryVo$qtQr1R4;{#&&f1L z=|%;}1qSo&BY}Wi2*7xlU<9bAGzDg*Pp8)4`)Rio(yo09`*?%Y1Z&wb!-^Xcz=?ch_-ozY3~W)2u3;n6`efM`V8nNAKv zbRjD31p=A-UEg)b9dFJyeCy~I!$UuXf;=q@w*cS=0i4F5%D7_889{uO>u>Zq;`k7K zdHLHw=jZ2icXOATuAvs%BoI1HLl5l^cP$d8if!d+DW@TdoOrS&N@8Ldl-0inx*8Sz zi<7cU0(nG~}PLj%afEyyHiWx4cLFv3f`5{zG5RG1;d03KteupI(wqt7wG zYhC#kGX%}xa6>1J7X}vgLecch#BgJG1Db0M!#luui~vW#nvLm8WH=eM+49`@X(Erz`d3?fq zNOCR!&j4}9aEI@ZZ2UwS4rDv#4%MHXbZEg_HH~~_H*ZGyD$wQVDOI! zW#jjKr7OBjn(BLUpM@RHn`sE^_cz0nlcq48CI_vM-1{WN4nW7?TLjVyq7OShI6vT9 zhqwF?fx8|^!%dxQkm1LMjxz{gsUdW*>PT%Qp?DSu{&fJI_xS<%3gOQI@j25Sz5^;0 zXAMzHL`>wgL?~@YBGT$qtwR+zih^Or-?o*3Bw+o*;0uq{O(v{phATz!WC+VYNAoe% zs-W0{5M1DjC{b80T0xvO*9+0Kkr|iNijlDHO-?x6t}>sv-KUd3El$WA0mz84gYl_?gW! zKJa$OgJ-!WjJFZs6<~NJHVv&H#5Pji#(}6jDCYOzQ|L{n9snNt*;X_N27xy zc1JsP$-0Faf;hoRD2=X@YPjo0jS)1#$V^HTF%ClFDNOS^`T(K5MZMW8dO~DN&-)F- zw~2#vQqLeo!A*!DVi><+H1pu^18-zfQ9n!Z4?a(ts6A3!t z8~qJ9Iy&;xpxg_2c*nr-vjW7upre5<+TwZTJBSE>qw1JKU^Xz=YHf)xg+ zFh3LyrSvF^D7ir!Ao++ez6Twq&@^#55z;{y$KG@~+n&)Gox!%*>P(?398P0Mn#UM) zUa~m3x~O142BvYSEvvnW6jOegmh<#{u?hsc4jX$*U6McvtD;xqiydZB0;7_7zjG>8-)??0{HLF@;{J{0} z^7R7nbwGHIp!VE35S)XCRJ768JjRfnz^>cTshq;L-RewbrKc^Xu8H3MOxsZZCgL~c?%vV z^u9QN9ElEdmh0j@Tt5vUucf(4Y9L)`*JB%)xJWvoc!iRz2MTkP-^GLQp zxDCaWkAWZ-!0!OMuA^PQgB|Xmrye@&R7k+g3zs6Gl3cQ500)JDT+x%ETBd;Mnp8k2 zO>^eANf?K~$gM^hTG-$QHq8c_wi)Iy^n=v=Z4MD+BR;-xDE{wPh2c{1HrEZmUju^Df&83JJk-P(jI${1GWGB#Nf@~fa9LYbtFzu z+~u|$D%ogB|DE|3q3I?)vD0zpa-}Vv06FH&9JsY0!EiB8JhI6&zH5qX`vy9V z@m>IU37ncEJV8I@?^e8d6xLI3-^O~-y6zufW6UI+X-zTa|B9XfVX@SxF&!YlFpbqN zOrhQZ;4W0*cFev(2c}6Y(YIOw#`Do#VEF|B{49E&31mPs>;cXfdaRO0wQ45z)xzKd zwur*vu)r-KcrP$q5BC1Ce^H7tVrZt*ZGMt)&KW#bV91#L@FI_SMNfp-Oz#W>cVU>f zFwG1b1V9UHM++fn1*P+F?wbJcnE<8>LOa;=(ZeS?a>Njr;(-NeaZ50Pln2+St2X~d> zzZ^=cp8A3pW{wbl-n22ZPaq!IWSUYhb82TU!aX`R*dDoxPn6T^;yG@V4uAe4|775v;3>d=yw?+?9HNDTB5Q^eukAwr6wSU{>8f1ijPVg5@O-^`vje) literal 0 HcmV?d00001 diff --git a/public/assets/logos/scoresaber.png b/public/assets/logos/scoresaber.png new file mode 100644 index 0000000000000000000000000000000000000000..47c058ee2d51cb7f58454dd4758f28c1e4a108d3 GIT binary patch literal 1572 zcmeAS@N?(olHy`uVBq!ia0vp^t3jB94Mv{91mT&J0{Q9l>dDhuw%pynL zSYPumnDeKC69t6xX|-^!`Q)q3di(WH;g(&2YLeAqC1=1k6Y)<4he z63LpSy8Y4GtAcxY8hDag@t~e<{F_vSI#aCX)*U={VqWE^YtR4exSp=;a%Qf^ncCuS zuPmjNSB7Z)78euiyCHGvD|_UvKGT^#>rSU_uD>a9(?349Y^93-o2(h(Iz`e?~ztBO~v)RWh$zAIiDy)5U_)%o>jer`23TPxJZ z!DQ}+EesU*YXxj3}Z|bjwMrp6QGIe>(o=-b{t7hG+3cpiuXIaLj>GDMzkF7UzJ;EnekbHzo5);`< zzrOT)gVaXfi_^B>dl#3N-DLmCJ^J;Vd-oRZzxyh`u=mGYv({k$JC9eLbkF6|-}hss zw`kdoEj3yz)$b(zs+@i3^DFV#JyNyed1tSh->zS*czf;Y)w|M8Of}ZOEfTl==&SI) zo&U6N=SHdKY{%hSa59)!v3Bd1{pa-Gf0CL$H{MpZ@@UrcsPEtV=H1CF)q6NA?$Wx$ z(LTB5SN|2xo)(_=FYfs)*4euxfR?Sj^-EXRC4ODf_r<~$k_=e92=uh=uI8)r9=yHr zPYtMh&#%(g*?(_cID6yZr43&g_k4V{QZ!O%VVT56C$>S1+-`3)n|&^H)mwc9t@Sro z<&~6_JU-aW&i-cOg-ti#udV5>$=gXHbKcBv?Il|q|?arGwIUf#d6zz<8{nK=pPyOMud&^I%Y5e>VacZx{ z&U4E$yp~>CS7Y~oR_{U{e*WDvXU=>u>pHdsi=2}SH>We-+i==LsIK_0GXIogIV}^o z_g}HM__y}=rDGmhTW@t1?W{R=EMT3zbV2aOZP^J43Epaxe;#z>&=lcv29{;Y(%~}f zkqVs=ip@BX@{PQ?GiDU1`_J3b9u{Xb^UT(3dD4nYV)XXkjar@?Qy9HHuhhEy9S_i# ejqje{vEOkzw~;-@WhSs>XYh3Ob6Mw<&;$V5B<0Tl literal 0 HcmV?d00001 diff --git a/src/app/overlay/builder/page.tsx b/src/app/overlay/builder/page.tsx index 93d4530..fda77d7 100644 --- a/src/app/overlay/builder/page.tsx +++ b/src/app/overlay/builder/page.tsx @@ -79,10 +79,10 @@ export default function Analytics() { id: "scoresaber", value: "ScoreSaber", }, - // { - // id: "beatleader", - // value: "BeatLeader", - // }, + { + id: "beatleader", + value: "BeatLeader", + }, ]} onChange={(value) => { settingsStore.setPlatform(value); diff --git a/src/app/overlay/page.tsx b/src/app/overlay/page.tsx index b400ac6..e892e26 100644 --- a/src/app/overlay/page.tsx +++ b/src/app/overlay/page.tsx @@ -7,7 +7,8 @@ import ScoreStats from "@/components/overlay/ScoreStats"; import SongInfo from "@/components/overlay/SongInfo"; import { Card, CardDescription, CardTitle } from "@/components/ui/card"; import { HttpSiraStatus } from "@/overlay/httpSiraStatus"; -import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; +import { OverlayPlayer } from "@/overlay/type/overlayPlayer"; +import { BeatLeaderAPI } from "@/utils/beatleader/api"; import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import { Component } from "react"; @@ -17,7 +18,7 @@ interface OverlayProps {} interface OverlayState { mounted: boolean; - player: ScoresaberPlayer | undefined; + player: OverlayPlayer | undefined; settings: any | undefined; } @@ -31,13 +32,44 @@ export default class Overlay extends Component { }; } - updatePlayer = async (playerId: string) => { + updatePlayer = async ( + playerId: string, + leaderboard: "scoresaber" | "beatleader" = "scoresaber", + ) => { console.log(`Updating player stats for ${playerId}`); - const player = await ScoreSaberAPI.fetchPlayerData(playerId); - if (!player) { - return; + if (leaderboard == "scoresaber") { + const player = await ScoreSaberAPI.fetchPlayerData(playerId); + if (!player) { + return; + } + this.setState({ + player: { + id: player.id, + profilePicture: player.profilePicture, + country: player.country, + pp: player.pp, + rank: player.rank, + countryRank: player.countryRank, + }, + }); + } + + if (leaderboard == "beatleader") { + const player = await BeatLeaderAPI.fetchPlayerData(playerId); + if (!player) { + return; + } + this.setState({ + player: { + id: player.id, + profilePicture: player.avatar, + country: player.country, + pp: player.pp, + rank: player.rank, + countryRank: player.countryRank, + }, + }); } - this.setState({ player }); }; componentDidMount() { @@ -60,9 +92,9 @@ export default class Overlay extends Component { this.setState({ settings: settings }); if (settings.settings.showPlayerStats) { - this.updatePlayer(settings.accountId); + this.updatePlayer(settings.accountId, settings.platform); setInterval(() => { - this.updatePlayer(settings.accountId); + this.updatePlayer(settings.accountId, settings.platform); }, UPDATE_INTERVAL); } } @@ -113,7 +145,7 @@ export default class Overlay extends Component {
{this.state.settings.settings.showPlayerStats && player && ( - + )} {this.state.settings.settings.showScoreStats && }
diff --git a/src/components/overlay/PlayerStats.tsx b/src/components/overlay/PlayerStats.tsx index d9c186e..55dad76 100644 --- a/src/components/overlay/PlayerStats.tsx +++ b/src/components/overlay/PlayerStats.tsx @@ -1,14 +1,20 @@ -import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; +import { OverlayPlayer } from "@/overlay/type/overlayPlayer"; import { formatNumber } from "@/utils/numberUtils"; import { GlobeAltIcon } from "@heroicons/react/20/solid"; import Image from "next/image"; import CountyFlag from "../CountryFlag"; type PlayerStatsProps = { - player: ScoresaberPlayer; + player: OverlayPlayer; + settings: any; }; -export default function PlayerStats({ player }: PlayerStatsProps) { +const leaderboardImages: Record = { + scoresaber: "/assets/logos/scoresaber.png", + beatleader: "/assets/logos/beatleader.png", +}; + +export default function PlayerStats({ player, settings }: PlayerStatsProps) { return (
-

{formatNumber(player.pp, 2)}pp

+
+ Leaderboard logo +

{formatNumber(player.pp, 2)}pp

+

#{formatNumber(player.rank)}

diff --git a/src/overlay/type/overlayPlayer.ts b/src/overlay/type/overlayPlayer.ts new file mode 100644 index 0000000..c043797 --- /dev/null +++ b/src/overlay/type/overlayPlayer.ts @@ -0,0 +1,8 @@ +export type OverlayPlayer = { + id: string; + country: string; + profilePicture: string; + pp: number; + rank: number; + countryRank: number; +}; diff --git a/src/schemas/beatleader/difficulty.ts b/src/schemas/beatleader/difficulty.ts new file mode 100644 index 0000000..f3d69b3 --- /dev/null +++ b/src/schemas/beatleader/difficulty.ts @@ -0,0 +1,30 @@ +import { BeatleaderModifierRating } from "./modifierRating"; +import { BeatleaderModifier } from "./modifiers"; + +export type BeatleaderDifficulty = { + id: number; + value: number; + mode: number; + difficultyName: string; + modeName: string; + status: number; + modifierValues: BeatleaderModifier; + modifiersRating: BeatleaderModifierRating; + nominatedTime: number; + qualifiedTime: number; + rankedTime: number; + stars: number; + predictedAcc: number; + passRating: number; + accRating: number; + techRating: number; + type: number; + njs: number; + nps: number; + notes: number; + bombs: number; + walls: number; + maxScore: number; + duration: number; + requirements: number; +}; diff --git a/src/schemas/beatleader/leaderboard.ts b/src/schemas/beatleader/leaderboard.ts new file mode 100644 index 0000000..42c8771 --- /dev/null +++ b/src/schemas/beatleader/leaderboard.ts @@ -0,0 +1,16 @@ +import { BeatleaderDifficulty } from "./difficulty"; +import { BeatleaderSong } from "./song"; + +export type BeatleaderLeaderboard = { + id: string; + song: BeatleaderSong; + difficulty: BeatleaderDifficulty; + scores: null; // ?? + changes: null; // ?? + qualification: null; // ?? + reweight: null; // ?? + leaderboardGroup: null; // ?? + plays: number; + clan: null; // ?? + clanRankingContested: boolean; +}; diff --git a/src/schemas/beatleader/metadata.ts b/src/schemas/beatleader/metadata.ts new file mode 100644 index 0000000..15e27de --- /dev/null +++ b/src/schemas/beatleader/metadata.ts @@ -0,0 +1,5 @@ +export type BeatleaderMetadata = { + itemsPerPage: number; + page: number; + total: number; +}; diff --git a/src/schemas/beatleader/modifierRating.ts b/src/schemas/beatleader/modifierRating.ts new file mode 100644 index 0000000..f1a21e8 --- /dev/null +++ b/src/schemas/beatleader/modifierRating.ts @@ -0,0 +1,18 @@ +export type BeatleaderModifierRating = { + id: number; + fsPredictedAcc: number; + fsPassRating: number; + fsAccRating: number; + fsTechRating: number; + fsStars: number; + ssPredictedAcc: number; + ssPassRating: number; + ssAccRating: number; + ssTechRating: number; + ssStars: number; + sfPredictedAcc: number; + sfPassRating: number; + sfAccRating: number; + sfTechRating: number; + sfStars: number; +}; diff --git a/src/schemas/beatleader/modifiers.ts b/src/schemas/beatleader/modifiers.ts new file mode 100644 index 0000000..38d45ab --- /dev/null +++ b/src/schemas/beatleader/modifiers.ts @@ -0,0 +1,16 @@ +export type BeatleaderModifier = { + modifierId: number; + da: number; + fs: number; + sf: number; + ss: number; + gn: number; + na: number; + nb: number; + nf: number; + no: number; + pm: number; + sc: number; + sa: number; + op: number; +}; diff --git a/src/schemas/beatleader/player.ts b/src/schemas/beatleader/player.ts new file mode 100644 index 0000000..b669254 --- /dev/null +++ b/src/schemas/beatleader/player.ts @@ -0,0 +1,9 @@ +export type BeatLeaderPlayer = { + id: string; + country: string; + avatar: string; + pp: number; + rank: number; + countryRank: number; + // todo: finish this +}; diff --git a/src/schemas/beatleader/score.ts b/src/schemas/beatleader/score.ts new file mode 100644 index 0000000..52321db --- /dev/null +++ b/src/schemas/beatleader/score.ts @@ -0,0 +1,51 @@ +import { BeatleaderLeaderboard } from "./leaderboard"; +import { BeatleaderScoreImprovement } from "./scoreImprovement"; +import { BeatleaderScoreOffsets } from "./scoreOffsets"; + +export type BeatleaderScore = { + myScore: null; // ?? + validContexts: number; + leaderboard: BeatleaderLeaderboard; + contextExtensions: null; // ?? + accLeft: number; + accRight: number; + id: number; + baseScore: number; + modifiedScore: number; + accuracy: number; + playerId: string; + pp: number; + bonusPp: number; + passPP: number; + accPP: number; + techPP: number; + rank: number; + country: string; + fcAccuracy: number; + fcPp: number; + weight: number; + replay: string; + modifiers: string; + badCuts: number; + missedNotes: number; + bombCuts: number; + wallsHit: number; + pauses: number; + fullCombo: boolean; + platform: string; + maxCombo: number; + maxStreak: number; + hmd: number; + controller: number; + leaderboardId: string; + timeset: string; + timepost: number; + replaysWatched: number; + playCount: number; + priority: number; + player: null; // ?? + scoreImprovement: BeatleaderScoreImprovement; + rankVoting: null; // ?? + metadata: null; // ?? + offsets: BeatleaderScoreOffsets; +}; diff --git a/src/schemas/beatleader/scoreImprovement.ts b/src/schemas/beatleader/scoreImprovement.ts new file mode 100644 index 0000000..a20b54a --- /dev/null +++ b/src/schemas/beatleader/scoreImprovement.ts @@ -0,0 +1,19 @@ +export type BeatleaderScoreImprovement = { + id: number; + timeset: number; + score: number; + accuracy: number; + pp: number; + bonusPp: number; + rank: number; + accRight: number; + accLeft: number; + averageRankedAccuracy: number; + totalPp: number; + totalRank: number; + badCuts: number; + missedNotes: number; + bombCuts: number; + wallsHit: number; + pauses: number; +}; diff --git a/src/schemas/beatleader/scoreOffsets.ts b/src/schemas/beatleader/scoreOffsets.ts new file mode 100644 index 0000000..d443d40 --- /dev/null +++ b/src/schemas/beatleader/scoreOffsets.ts @@ -0,0 +1,8 @@ +export type BeatleaderScoreOffsets = { + id: number; + frames: number; + notes: number; + walls: number; + heights: number; + pauses: number; +}; diff --git a/src/schemas/beatleader/scores.ts b/src/schemas/beatleader/scores.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/schemas/beatleader/smaller/smallerLeaderboard.ts b/src/schemas/beatleader/smaller/smallerLeaderboard.ts new file mode 100644 index 0000000..584827f --- /dev/null +++ b/src/schemas/beatleader/smaller/smallerLeaderboard.ts @@ -0,0 +1,5 @@ +import { BeatleaderSmallerSong } from "./smallerSong"; + +export type BeatleaderSmallerLeaderboard = { + song: BeatleaderSmallerSong; +}; diff --git a/src/schemas/beatleader/smaller/smallerScore.ts b/src/schemas/beatleader/smaller/smallerScore.ts new file mode 100644 index 0000000..9e21033 --- /dev/null +++ b/src/schemas/beatleader/smaller/smallerScore.ts @@ -0,0 +1,14 @@ +import { BeatleaderSmallerLeaderboard } from "./smallerLeaderboard"; +import { BeatleaderSmallerScoreImprovement } from "./smallerScoreImprovement"; + +export type BeatleaderSmallerScore = { + id: number; + timepost: number; + accLeft: number; + accRight: number; + fcAccuracy: number; + wallsHit: number; + replay: string; + leaderboard: BeatleaderSmallerLeaderboard; + scoreImprovement: BeatleaderSmallerScoreImprovement | null; +}; diff --git a/src/schemas/beatleader/smaller/smallerScoreImprovement.ts b/src/schemas/beatleader/smaller/smallerScoreImprovement.ts new file mode 100644 index 0000000..2c32ce3 --- /dev/null +++ b/src/schemas/beatleader/smaller/smallerScoreImprovement.ts @@ -0,0 +1,9 @@ +export type BeatleaderSmallerScoreImprovement = { + score: number; + accuracy: number; + accRight: number; + accLeft: number; + badCuts: number; + missedNotes: number; + bombCuts: number; +}; diff --git a/src/schemas/beatleader/smaller/smallerSong.ts b/src/schemas/beatleader/smaller/smallerSong.ts new file mode 100644 index 0000000..3b8811c --- /dev/null +++ b/src/schemas/beatleader/smaller/smallerSong.ts @@ -0,0 +1,4 @@ +export type BeatleaderSmallerSong = { + hash: string; + bpm: number; +}; diff --git a/src/schemas/beatleader/song.ts b/src/schemas/beatleader/song.ts new file mode 100644 index 0000000..51c8898 --- /dev/null +++ b/src/schemas/beatleader/song.ts @@ -0,0 +1,16 @@ +export type BeatleaderSong = { + id: string; + hash: string; + name: string; + subName: string; + author: string; + mapperId: string; + coverImage: string; + fullCoverImage: string; + downloadUrl: string; + bpm: number; + duration: number; + tags: string; + uploadTime: number; + difficulties: null; // ?? +}; diff --git a/src/utils/beatleader/api.ts b/src/utils/beatleader/api.ts new file mode 100644 index 0000000..9f4a2f2 --- /dev/null +++ b/src/utils/beatleader/api.ts @@ -0,0 +1,126 @@ +import { BeatLeaderPlayer } from "@/schemas/beatleader/player"; +import { BeatleaderScore } from "@/schemas/beatleader/score"; +import { ssrSettings } from "@/ssrSettings"; +import { FetchQueue } from "../fetchWithQueue"; +import { formatString } from "../string"; + +// Create a fetch instance with a cache +const fetchQueue = new FetchQueue(); + +// Api endpoints +const API_URL = ssrSettings.proxy + "/https://api.beatleader.xyz"; +const PLAYER_SCORES_URL = + API_URL + "/player/{}/scores?sortBy=date&order=0&page={}&count=100"; +const PLAYER_URL = API_URL + "/player/{}?stats=false"; + +/** + * Get the player from the given player id + * + * @param playerId the id of the player + * @param searchType the type of search to perform + * @returns the player + */ +async function fetchPlayerData( + playerId: string, +): Promise { + const response = await fetchQueue.fetch( + formatString(PLAYER_URL, true, playerId), + ); + const json = await response.json(); + + // Check if there was an error fetching the user data + console.log(json); + + return json as BeatLeaderPlayer; +} + +/** + * Get the players scores from the given page + * + * @param playerId the id of the player + * @param page the page to get the scores from + * @param searchType the type of search to perform + * @param limit the limit of scores to get + * @returns a list of scores + */ +async function fetchScores( + playerId: string, + page: number = 1, + limit: number = 100, +): Promise< + | { + scores: BeatleaderScore[]; + pageInfo: { + totalScores: number; + page: number; + totalPages: number; + }; + } + | undefined +> { + if (limit > 100) { + throw new Error("Limit cannot be greater than 100"); + } + + const response = await fetchQueue.fetch( + formatString(PLAYER_SCORES_URL, true, playerId, page), + ); + const json = await response.json(); + + // Check if there was an error fetching the user data + console.log(json); + + const metadata = json.metadata; + return { + scores: json.data as BeatleaderScore[], + pageInfo: { + totalScores: json.totalScores, + page: json.page, + totalPages: Math.ceil(json.totalScores / metadata.itemsPerPage), + }, + }; +} + +/** + * Gets all of the players for the given player id + * + * @param playerId the id of the player + * @param searchType the type of search to perform + * @param callback a callback to call when a page is fetched + * @returns a list of scores + */ +async function fetchAllScores( + playerId: string, + callback?: (currentPage: number, totalPages: number) => void, +): Promise { + const scores = new Array(); + + let done = false, + page = 1; + do { + const response = await fetchScores(playerId, page); + if (response == undefined) { + done = true; + break; + } + const { scores: scoresFetched } = response; + if (scoresFetched.length === 0) { + done = true; + break; + } + scores.push(...scoresFetched); + + if (callback) { + callback(page, response.pageInfo.totalPages); + } + page++; + } while (!done); + + return scores as BeatleaderScore[]; +} + +export const BeatLeaderAPI = { + fetchPlayerData, + fetchScores, + fetchAllScores, +};