From 39395e173e5351e043c394ff07cf0b2c0d271c4e Mon Sep 17 00:00:00 2001 From: giteaadmin Date: Tue, 26 May 2026 08:33:40 +0000 Subject: [PATCH] optimize flow --- .../Create Room/FONTS/NotoSansThai-Black.ttf | Bin 0 -> 47412 bytes .../Create Room/FONTS/NotoSansThai-Bold.ttf | Bin 0 -> 47480 bytes .../FONTS/NotoSansThai-ExtraBold.ttf | Bin 0 -> 47524 bytes .../Create Room/FONTS/NotoSansThai-Medium.ttf | Bin 0 -> 47472 bytes .../FONTS/NotoSansThai-SemiBold.ttf | Bin 0 -> 47552 bytes .../NotoSansThai-VariableFont_wdth,wght.ttf | Bin 0 -> 217004 bytes www/html/Create Room/create-room.js | 1 - www/html/Game/public/js/play.js | 26 + .../public/js/play.js.bak-20260526-151951 | 18704 ++++++++++++++++ www/html/Login/login.js | 8 +- 10 files changed, 18734 insertions(+), 5 deletions(-) create mode 100644 www/html/Create Room/FONTS/NotoSansThai-Black.ttf create mode 100644 www/html/Create Room/FONTS/NotoSansThai-Bold.ttf create mode 100644 www/html/Create Room/FONTS/NotoSansThai-ExtraBold.ttf create mode 100644 www/html/Create Room/FONTS/NotoSansThai-Medium.ttf create mode 100644 www/html/Create Room/FONTS/NotoSansThai-SemiBold.ttf create mode 100644 www/html/Create Room/FONTS/NotoSansThai-VariableFont_wdth,wght.ttf create mode 100644 www/html/Game/public/js/play.js.bak-20260526-151951 diff --git a/www/html/Create Room/FONTS/NotoSansThai-Black.ttf b/www/html/Create Room/FONTS/NotoSansThai-Black.ttf new file mode 100644 index 0000000000000000000000000000000000000000..aec52a6d7676e76857b9b48bc1e874527720b726 GIT binary patch literal 47412 zcmce<2Yg(`)i*qI?>23(T4~j`NW1FN7VWO8C9B!8E!lFz70a^RZP}7s0UKi*(;*aN zO27~xAvp905(30Hgkm6}1xR8-Nuef?gqQ@-?)#s)chzjl^M1eg^Lp-`ojc{sIcLtC zGIvENA;gS}f{^x>*0zknjM;<;aYE>v_L*I?k3aX&`v!VhHhWIfoPX1|R^j_0d~fXC(6i~zSIeqV7xfj1RXu~7*l2ie z$6d2({f?EF-{0Iw$m9zM*?MGkU(X8r2e&_m=gaUsx*7$_V&P?cufzBJ)f=k9Z1=LR80bag7A^ed>kuTgc|951Q+i+hxeC@o-}~j&mmht7Lqdo z59@u98mWzDQ9oUQ?|QWPuUwLcvSQj!XVMk)T8yUpmeCm83_ld3V3G09QgR)6n7m7) zbS*t5IE2;04Z=HOtGHJ@D*i`NtyrY^f#M#;&lO)P<8?> zahM$4j-8HI(#zBP(|?uzX8H&CoOIeVGBcVpc4s`D8Oq$3`QyxAXMU3TrAzIyx$;~k zu9&OY)#X~`y4v+C*GsN9T_3pqot2qYkX4p7Ijb{kVb;p5fvjCw7iAsHdM@krY%x1M zyCZvE_VVnF*>`6@nf+dNGDnx=%<<(^&oELKbnrq4p z=GNy<&7GUulY4dUOSy06evtd`Jep_7%gkGuH;{L2-b;C3xixOP+wETI9&qn+U*taM z{-OH;_b=Rk^{72IPoAg56Z15Cx;%?K*L&{p{K}i=b$C7AGHF3m*yWS$Sx=>SXR(qaCX51 z1s@cA;n(?#{2_m*{~Z5?{vZ1v@IUT9?tjhyWk3sv2FADx7_)zdz@P*(T!GDIzL-Rs6hwcjfEOauQ9*%_Pgl`T1H2g&P zwTK}yBXU{fw#ZK+uSUMCvQ!mT1*;}it*F{mbyn4Zs<)!bs4+Stx*)nD+8;eD`gTkm z%ZT;FHpgy{Jr{dD_NTZvzA}DU{L%P_)#mE#>Ogg& ziVtz$2)U&s<;WQzg?>P8CwGy1$%EvlG^Xklc;&9wLvBUr;Sgr@37IltXq@(0q)nmpJlAp05`L0SJqrbzyq;L39LXr$u{9nRA$XC1< zLf+L9@^=5*kkRjKLGEww7~ZkIefir5-kSaU`#5$9SxX@eAxA^FMrfSY(Bt$3{VV;H z{*!)2zo1_UD#0$egi0YQ#D!&S2w^!(!bV}MuoJnnk#-56iqKrdO7>g?l~|haZxyYh z7t#&1ke){p^a^?|-9tsXpXLw;-AiwwPt)!6$MlD^kxrtA>CH4m*U@Zx6R21XdLEz( zvX5L&t{~TtL!kd{e3K7JE+mm9EW z-%E~=pMZOAB}d6qihCZ9v^_yWZIk~+Z4 zrL>$@(0R0*&ZcufoAq=H?WddQ*>p3w{VaMfJworM_t3}b9rP%rU#VSYxJk)P5t$s=?*d5o?G@2@3~(>3HUjge>QHu5yxN{-P1@@sk}d68aC zenYP!FVpMDtMmr)3ca4ZPH!Xc(EG^=`V;ae`ak4tdLQ=N2g!T%XXJ167v%5cAM{D` zFZvYuC;b)qExnPvL2sunY9%|Uffmt&bddgtcF}h1`&rNiR$%up#>%@E>&=EWm_diI zt2;?1aghvYK{e0=G|)WyNE+Egw9s5OLX%k!y+;onW;5A9Hjzn?BvVKmG^kdtamJy| zOoi6sAzsLq63B{5P( zib(|tK?hk4eP=hZkiDddY$F523hiw<>46-nhh)npAJCtZztW$Q_vs_#6M78tql#Wk zFQH6>Vyjq7jswrCmbU40$dTS11MA6=6@6Pn143bw`| zr=Wczuf)vLFqXP$=FBN%Yje{aH|d_r^D~;JAU~sPx|=k1HqAr6yK9D<$y2n3e1P0l z0&A@O*b;_Isf3Nd>Wuv^%HPLALFF=SPtW=xaLvJiH~ zdNPM!JNpM#Y$Q_#2P=c171Oy*5;~A`U~CFnu&a$2(cSmyfkK}F{$z3AnG??ch$lKpzt|gG0ccJGL3>hF0(*_;1gzc;lt?fz zY&9aKV4}c2>!%V)g2{lK0qeCG>zl3IDy-Yo8efeS%zDT}lKNw93z1U&Nm>ep%t`g9 zLyp-N?79a@hMKY(sc!X5e3f9nVX}rl zm$)I-g7L%HIgQ0VqZPAmr|@>}3D7BdJ4Uh9cw(1(DVJ#&1r_T6`&{5>KJc>`5_>6X zGMiPR6w^^g@xW*?4Y-!LBU`Q`VM&CXXVRWwfKj1C#xs+J5=ZU=F0y5g6p$~#6SgXu z4?}_6XbHnwsuiPv5s6~m3V^p0xzsZ|x7AbMR9uoUYd%#&z-X*jRw8idB*7eZ# z^fm=6vJMoh1aC7qCGKGuW3yR}c56T>*2^+}U5INKBQfp_;#vaE6hOTqV8t(EK!Qj> zEhqTb4XiPqs2ky2#u=Pvxn%q;u8;_jlQ#kaCt%J4{uo!)0;dx9vb|K|TP8zag=As+ z*hc6LL}(*(q3`j3;&P%AI!K*x5vdUxNWQQIX(wqB?;)ka_lTXReJHC!sziF6Gzx!3 z`4323NXPfRq(xXu@@WI~OeA4F%6^ExwiCB-2=_z8FaCg(qyIR4fp~?t(Doywl_V$C zzxW>8eG=F$he5@}Gq^v2`)k-MvE~vLxWmttIEeC};68-=uW)}4DS-SA+^Y_+}NBiK^-S? z3A<1qNqm)*Vt*;%@IOjAXg^WW4Vdo>iN|p7K>x2Jb*Juh0IQ26;2%r;GgXE=e2c73 zq86!5q=3PUF&@TPw;?fnttUAWT@oiy_f6FM0`R)UXGsy86X;ZcR3t>vmcbT>bS4QQ zg-6sW60Qa3d;xs^9uk26OMZm;T_s?j6^xMNB=NFv4k=`~5N|>sVe|z|i}xqLWcP1P zw~`scg(Q4R`XkDvWD*LHmr@JvETz8ly9lk1m;I5Lge>I$jQ++XF_&};2{J>39s*6y zE2U%-ciZT9xqMmjKH(np!BQJf?ecww(3AX!_%!;#c&DTs5(W;!BD;S>0$paX=fdsi zdu(a~?k1)#k}0IpAnN~{O#M#jA?A4{rin>-g$ST2IVJUyTw!eL2Mxz0dMl(0Nu0&X z#2=166XClYsb@(32fS1!WRi4o4=Lp3d&y*I%_1F!Epi{pMRLP_sYY_rKd?TNwXnnT zL9=3>bi6EQUrCgP%5yzLE;EyZ7(U{i>_(sOvD^XX@R?Itpr*JpFvLrTzJ7WI-sp1x1g@x2_$ zGBSP&j=y8>9B;!faTxAIUw2~7Ux9wFz`A4o{s^>~!Qtwlt)u~|k+vqELi#=O&A5Mo z^iSkNv?cihOTtxXhXfmtrAhL=k&Qub0e`-a-Gn4Nx$Nv9t$Z9dRw{J>H!OdUSg~%4 z$0o?7Vp_`KxpHhd`YxiEk_^x)L%2?ALqAInW6qP2LUa}8w2EYj4-zY<4d}z?3|!Fv zV$J;*W9dn)P!4(YFX+zyf`;=2(IQzv#~hkZYUyt=A1URsJEJL`$6@Igl;}@Nxv4y8 z6QM;U8{_zqyx=n>?jia-Xe}oWAA9x@qfQTP{lTM8SBZkO`{ z*-8aDFA^`2+hUJ$!P6quXVDVK3^p!hdYn8j@TCUUom5_i{pe~rUyeE#$@z*=ywOVZ zvr8@yV*lj!9Qq8QpOtcX7<@QK&PPV)!evWbE{~#)U(Uw>r(Vv-kt5LF`r$L!0Dr-H zco_PSTLX=$AET|nw~dH@^dWa@Nh{e78(;wKzUkRy^t2xBQ}@&esqZs*?`xnBZ-9lh zhOC0NI0P?As_)UVIe4-L@XW_`1?sFuu5YBp8jLdpSlmb}0CO)OTLaI_5PTxN_^_D_ z@v+>9_zfV}2QQ5qGj0a{*5KKCTsLBTH(IRcbqDahhvx=SI+gd$u;PY(;|2v7-rRUT zzI6{;u{sQ&>v8WzZWHbdk3+mR8)Xpf*Yh6OsO;$uU|>1N)*Q50!&~>EoQ=(9+b`c) z|87!&w1b!Jkl|)@9739n){JTlU#y?W=&1)#tmBksHQ21BI_{BL4C_OHa}d@*1wLaj zI0*e|kV90C)&qdP0-mTg&`Y8TgRl)x7=~Hj{g~SjJW)Ly&u+A2H5h&+I!GMl2F)cN zld!lN_+hQ5qF*=q--PdMrlUPGUKs766wl_N&H#svjqZjm!R*^|-pH*&_Ba8qa7Q!3BER#9|BI?DF45~P^y)L)osAaHo&)mV~f#i2cKs-=GBi< zhW8=hWd$JV9~p6c+f{hd4;nC-N87E!w;{COh-Zw(6|j>S_plYU5jn<@%Q2c8-bIG1 z(XrilHd>SMS|8@T0ezoZb8Mds$5PEx>rF-b9*mq?QByHuFM3Sz=qMG~cua;c7+9MD zzA709v2{8+Q?~xb_mSe^6rY!k;q-}o5yIG&_=NCDlegey`jm0_9H*aG^F@3j;Qd-7i43&sBglHHspelHzV~7M*Bi>L8Z&E#?CCoF~M4=zj7TQYN z5Otq|2uKH=MyDf=(h2|kOjz$ylwvM?r}OC|L7{ZjG)a9vFPLocJ3 zLt`$X--Dln#f*Lpz415nN<=QoNI7B{6^QK|#F>J3!5KkBgswvzV2>5I#6^Bi?=LnP&Ah>e)&BZxR$Oxh9AqKHad zMju1eRE>ztstK3%wTJBHgRx)>Tz()$tjHqLr<3 z`*1kO%WyA8WJvCNvXR$|OfKKpGceG4mH0;;M1Q5xMtpT%OH%5=P}l(Q%E!GUCJI5g)FW z`xr;cDjDrnDYB&Snp%P>%*abI(kzpwSsqjBGy)zUWx$|}jGzpDP#z>$sps4qtSn#O zH#jH}JSdM8Of9Ey+R&QyEBbo-H!PQGRJKWjRJM&>NI`iaRY`+{CQCF8weZ10m6aTF z+<7_foKr$o%_-Wa7*6JikPK{Piv(n4%NQW5DpgI(2l}@5shc+R^bYiIR5$gn>fhM6 zPTRbqf2gOow{PQ+s-?FFPjDOP?-^3H@^9MKk>~1Gsj1kyah1ANYR)GZ4287qqt$?- zU~6@&V#@NK0rgaQxT$07hoh?LJ-u6o`c%{TAW|>k$*NANv~x^pq{`5>ddtRDJp)@d ztnb+}r0$Y>RCP;ryT{b6imGPwQr=-G7+1|1eHscjspfJV80L=aaIOsETnR+w+<`S4 zSE=Unj&$>T*9`P-*|2hb-}bcmE7tT4^bM{VRLx&C(6hBqw_x<4Y5^}&E#%F#3rEnQ z>fvLi@p8@+J@R-xV^9*7g>ksDS=&3(mQN!bk_a7^<#{+HF9yyf{f$CV8%5v*lWgs9STdZ z1;a7bdOkzd`caUAVTo14l@ZlO>FLHXPb2a?LUBX?ID+&`Bc=6k?Hd^E?H}k<4M?pA z#wpHOQaJG_O_-jn)bUC#$wdG~Y6Ah%NGDw#YQ!!fC#Zccj}kO4hV(BQ)P8 z(R|0~L)8w6<~w;a?amPtnmGhO;f5Y*X<5eP2e96T`qCr-HuSH6q7MT=3w`q;`3y)0 zKLgUi&wzCBGaw!O3`l&!84v~9iO|=SNce|y9qcZgQop-)qIrKc~s#>$<5+)l>84$zUztOJVLC9 zz*$?6aDx4``S`ayPqk-vP-Uc#vE{7))OR+Hn}7e`ox)C0448z~%g&BZ0mW3{1UGbO zJ9KY5!$;zCT%S(hv<342!1bv_l;x97v{1o^*TQsY*eJ^w?^tju-h#NIC4z()>V&%# zp&ozoTR=O3)?gh7=nH&InRiKm{X>#pCBIC*O;pK~?1S$}xPHN|r~czlDR|fj{l5#G zGz-=QPQ<`|QR2gBs6zj0d_;`NPWYtZ!%ipY@!X7$3UjvOi4z|YmQV)jX5u3PBQE5# z@JWMplZTofe6--y2J|oqA3f}|Mzn3hM+dvD6?LZIBY>m9N3dOckzauiJF(V>K3C$S zh5fep}4{i72!_J%-QNm;f`^U}{AKn8NH5*Ey8PS;oR*MOWs!XN^?>|ZJm z{K)f7-aFFAH_%LKJr>%L9(g~>CsX*AYL3TuYAg;xYTRY$?Ie$er8*zEkF7saI3bg& zlY9iTWHV*><`|V{$-X}c3IN}X8&b2J(9MLeGKWnl{f4in_5Phwe}v|zJQ<-3dy?EW z%1=^FHp|mMjx!e{|5B@=ZKULs4ET54+8Y0DVs&t`j7RCoc*;(!&A!XDsFRF!gGap3`jRSD^C`{Ls~hulI8z|>S? zhxfPxCzPhqX~<8f(~+M63~JyrhStEb#=XR7%djSLtWl0N%CSZ{)5WFGtB0@ckNb(jW0;dNpt@G7m4jq|B#lf|vFV zjD8oquOfXAo>h_4K!G;_o`NpG&c899GIJUzIQA7B`x?BLz}`Y*oF#C-q!Ie+BfvQ0 zF9H0;sPY^>wlTCZ!4LU6lrRb@IfYbsZ{b7i(W7+IaXL}Z=?hTkD^Q4X3QPxTPJsL&7@O!M0gNs^m8}Q z9}=$Qp9k@D{O3xc1vRcjsZ?Y1T7!~G!OuT6f_uy-U64O`o8@JEjsmXjtUc~8W8Fr^ z)E(0fG;)HrmqTh?LvF?zxdr}jC%GRV7wGr`B*kl>SRtoXDW_5hp7#WN{%JG~CkS;^ zhx15gYKEVm$%|UVPwGh>r$9Z&eFMkzB*Zh$#p#?25UXgy+bIvg_slTY%yBlA;|#V3 zyzyt^EbIf&5MRS?@;cHRNPmXR{R`4xk={dkAL#?6kMQhcq`x8k9qAJ!);fgNAEWih zX#Fu-pG51EXnhi`Poninv_6T}@1pg)X#Fl)zl+xILJRLCZS(*%X9ZT39jnO>>f5n~ z?7-Cn@RTnAHjL>1N%a3D`hODre~kV=M*r}ZBfXFG0n%yvCyC^{=pS|kzY6&Nv6Q1z zlPBpkz$K6m(K>+~WFK-ci;ylxx&-M`q|4BQ7BK3Oa*|KdFw!36_u`I` z=ta0+4mc_ASBW|&QRgI{pTx71c*bti7WOnW&Rl>^H`DS(^ z9Ww#~3n{96J7zDp;4nRZ{m*AQ=~ zPzaFmVllWhiuLiJaJTRy;a1^B;h=B@-a+1nx3abi14!$ImBKP%p)g126s8E1g*v>; zRVfq;`9dzMguyyG$n(wrItm};kV0jTIgz@ww zuXCK&k$Us3meM zusr>O=VQDaze#|SKk)onJTKvAC$Ce=%bA@>h4~y0VO}zyV{!}2ixU2$yu6XuoXYW; z&-0X(Bg)5mn91ufomYUAV&}97?q;k3_Ex(VayJWQ?5%bStZN@Ec=kTK6X!o_V1={T zXC^G_Y0%r)d+R>f%$xBx0EC+g0w zca=E04mia{{e2z$Ec*$ z>#st8(!5zlMO!aE_o%P1(06pVY46!IlK4eQMMVj1N<4q|UbF?@3wc5qTucV50F(n@ zF#s9*)KyW2Oa{Nox2WuO(dBm(G?m2WwfC4D~#4<>rG*&~F zcUpF^)}=G(&4!!|e~u$ZrOtHe9{2ejS^D!#>v}W1k@R%0#bi*b1}%oX>Oxn!%jxrH zI`eI1zGjzuX?ETwwYSrw7To&6uE=JcDLW^H3Av2HPaJ6o8^mxok?l( zq__p+G^EA>LSg}+MZ#|i37lZy;($->v&axQWh{X%F{euPYSq+&aFDL8-@ah}K-`*V zcesyEIisc1s>`_Cl$Yu8if#J*9IHcPRhw*at*^1HBwlS+`0cSXBB7;|=Xyff-a=ZR zxMJaoc;P(Sm6dPF%=FBlneGyYO%&H@;lzZNkjxk6175sqF+a7U01zXe3=dmuBWr2= zY%IQVfh|7AtVNxTYwaFiNkNX#rq8g+;1qi4HfiEfxBQfyAq!e2MjJh;!c3)Y^j5Q5CgW;n-e^z0- zDK|r7QCrPHZS&c477R_!syeeVaSNU03%KfIh1Gc?y-ckFF4&5*2vcyPk;#BSSS`uI zXqc~O#@8wzBMQcH#=;VlM>qx9GTRlbQ`?&BW)&4IotI&@Y3^@tZZ4@XInpw`cB|E9 zQ1<2L%rKQy&JV8YY~Ik6mTsB8qrNR#k)3Z93bQjD7NbiwHG67l7dB{t%M?LafYpz^ zxCasmeC~9nWHZK72bho<3lu{4sppX0UJ250*g+*}$pNS8ZRHi>W) zi#Y7e6`WzfjE7?DK89}Txx^TtXHU&j%;mqJX>$E>XmB6y+2>3SVj+3PtG!F zwh4mO61KO;Ls45*Rl3ig9dEbO8-|@f|2bcQ82dwvjd;qSC=Jpeq=rMz78ZR0KO}`w zP^&jDdDxDh@e#x{gI-a#rc|jjYD7V)r2dEdA1yPaDHNhwtE~|7Z#9<2J??n9`R1F= z<#D$sUTUPPhMh-3?b+7is{A7?Mi&Pst)s6YnvscJnQ@c6sbbD55RUC$oE|YwS!sWx zvwF|2*>}yseKlvV=$pv}@N;$A-EyJKLtWw@+{Dtf(mo z;;c;xG+f3l(3YpbY9WIXuT0=szKxIY$2j&F-n6D*c1u3w|84Ef>49=bUarImk-QN` z*tBW#x=D~e?I3wpBtJ!Si%fFxYYT9_5c_8u^g2))dteNl=LoZz#@PPnXn9n5{%;a@ zTooqH25r~79G;xV_n6l9y1e1jZOU0rudSqjb|vn{ zMhv)WfW!@uk{F+@HE@*^>x(g0N=~YMl1}GvqCD*2lG9N$r#aZ7Q|elREwdjt84WgC zD_D#MtHKhjuv#m_R{Ep%mVBQtzok8K23?cousfZ0N7i;4$h_Da^RhQk-I#d;s1GfY z(TxeK7#3K7tBVj)TvlS`G4@v*g%OQKn*XHz=jgMYN~4~NX;zKDa<{>#Hfzi)v{s$Y zrh}eRkp1|1rcJ$iS9wLI(qvUrQLK(sxST3=ez&J6*J{o7^col?$F!)E_eRXg<9wpPnAybtIeiylg%ph zI_x<)4i&gJ5Tiwj+j=%L%9oQiaSJ`lR^c{z{Yxq{R>z2}VY^r$?DRq2s=3T%8>U0j z=)-Dg)hrnmiFJLxX|x(sKUL6MejE^RX9 zWOkHg&Enag`e$mXDJ8n2YhwOHbRTQ=<)ib)k zf8?Qi(`)Oi+<+a*9W?JSJS)(iCu%|C6sF1LOljM(aQ>FMqg)}HuFuc7cG7fNHG^W4 zxQ9+*%8zG;qTGEalPTV0KD`RPi%c%f2fqd6E>n8B<&~pH3kvcgcIUiU?c(SO+L$ew>VlPQgSBD$K)Lz0iv1rO3;af7ZRn*vB$^~h6 zS>E}mLr_prWzZQ^-sK*R*`gK&wV+nzmv_u;4LumH$;s0y>Ywwe^m>&-Wi+S@g#2e5 zb1JPqpU1Lzae7&KW@x6PAzZX0lCM<8XKNMO;z(9v{uW0`%;$@hIJU@8{sj^g7B`1- zWP^pq!nfKe);~9!pufPN!ZhS95bIJsv+gOKAczWWy1}=h&}7jns7j$z;0M2=(~C02 zhoW^A*~-dSy&8)~p|Pg=_6813%Q`9A4!8Ex|73EcHJvp1EhZavMPTW3! zN&nVMwz;!iHmyTru+`9|1#XZ1I%BRqbVf_!9&L9=*K(a?$Mtc}FfYqmcI{X#JH?D#6YprQ=dg5hZTtTb zEO%3D;_tV8Cs??BBK`yvK@_eP)8=aQw^}ChhBCUNI4ty9lQ$@Lg5(jl6Sq4& zRofGH%o!XcbXb^BKD?8a3yEih)ya27_N#~Re>7u72C#pBe&~19C9MABlh&0hi7@31s{Qzv;Gqm(gU=}Mz8F&k=)|XhgoN6aZp%|ccH!TkdbCdXJi%)TM=Q$U&1vR zdfk2)bB<%e>fFjqdr`VsV^A6mSoy<0;bZZZO%q!tLZeMLO>Bx$hlEwhr+|qJLYk@? z1Zl&NeGE)VXRz-|%gZq9RA!aUJ~<;8vt{2UtcofOPMg`N5T{x6h2_=)eKw=XuY^?` zuiwzy@NXve)<3Z~i=X$Fcn@%;15;nuTL7^ALzC?^rKwFytF1o0ENpOUt`X;#i8{O4 zpjRndj9QQ1oTqgzgB(u;gx=(z(I;Zgz!TeVoH4FRs?n1Vn*wGZz(zP$4kZp(H#Wzc zt2?Uo8gb!UI+IzGr^wZqOa_HorPLG(y-l%rGYtfUD!mNn7>ynbb)lZE)0fUH9~KOqi9ln zNi2J|!)ZqBz?jSUmJ$DUdaKh9#vN%-iQoMdo=QBnq+j4F6?7*0ntZU{(vBMfJA3*fZa z>}rEL&sbG3b@7s^MfLcVh5E9hmZB5F8k@ypbGd95I89xmu-RZgvv1}6%&e?TS6j>Q zH5i?F*OIMKF2?)riQAf% zLM>+zD(FW#LqH?E?z$&di#oH~Xz;m}TBSRq)DZ8?_7zkW zn7ttQx=jp%!zpyepf!cldG;V2eP z+O&pA)22;wIGxOz5az@}p%_b(9agJ@C7f=>FQ&{%{898P)TB0P1o+HDa}tqY?xEqn zM%ked@N*x{s5iuE^NG@qCgX~+^Gl7>J)WsGp$3yxlbfxy>TfK~H|7|0^KYK5NYiLc znly)1nWk`>!Jjo}y1HAF| zK1rZCD*H8LW9Ym1Bt7ZrdDf#>W$3Ikn$DPRaXWJgzS$?av(TFz^3aUo6Y~n=3m4wy z@|Y&NB$}QLyT(}N&YDP0bB#5leOAHh-LmC8jxo>VYaQF~-?7jJzXDkQtdDIm?|ItF|bfhML z`l~c%oz`eD8TGcZhO*$C2(y1mv46Dy3K4H4r|ci@q8S@K8F3Iz2$j%%M-x}1!X;xY z@38?A$pS)@q#!=&R8u@}+PJs~gQxrdEj*GH^c~@0GCW_r9{7>%Am$TGnL&)xWYIDK zW2du&_W4RAGiWq+gA;ZqVIM%Bnmtj^ z7bh0n8LY{aO@^h1#^h;LUMPo~41Sj2OOq zjTr`dc^+L*5cR~{-A9dPH4nwnqi6XmaQtq?nrnqih!E z;vH~#hhyW6XgQM0hp9MlK}uU68|_mJp|V-b*=}Ek-(s9wKff=>Vs`3Mqp|=XU6JX| zv03sQcGzGCG?{Jfg|*J~oZQUB&*b5gN%kvm^eWM>^5gdf*xw&O|ox@sr6W22Vsx4hPLr~#Eg0D8ACz8uL=%bpPKph zPodzl{L=0Jy^7{KCeZJay2!M;0}69p@T}9&FY$ijOYDnB!6mSyBaH3fz+?_=$9w6} zL)FGQUu|jP%T8!uT`EI~e+XFrXJQ)mM>F=v1=t^FVnz&WzZXNmcs8H;NZf;|x4x<| z;TK@zO+TE;p{kl_4gA7f83h7^X%KNsXf66@ZOeaPd?4@ekRcY?~-=@2DEd;A(}?C@3A6O3fnSm zh0|14jqOIkC&zK&?50|!Mz2hFNt$>jbarb&ydW!R6SW1!ma3T5WxTtreNN!}X==4b zpxI8H#h+Uf<0^Ue#?JcrahJoFk)erKeW))iqMH+UOkEnb(_FjW-~fzlPyQG%A}#>! zH6UvfT*NtjaP0adeuL{z)3Pr2EQ`;UlbxMubJ2@}*JftuCk1;Ktu+N}=H_I_Bhkvt z3|jH1&wI|gf%75bn67XLE8WI4GxR?37>Oz{Q9#qK50A#iSj49rbA^J=tN;jf z$k}fX_%OSf`=8|_Dol8=BP#pO&GxD@#)m`}Dq)%<{*#+om}7T}3a2v-=TzL;JQU<_ z@oQYV(+7ga9#!c!D$+tl?VC42|6xAp`M{SIUJ%6i$9bV8#3{Kl9-H8Y9*r@|4(ZKP zV~ZRWrKftN-%jB$<(YPsYG*Z_)-_FkoI)nn<_Y195%1-W1M&Psz_DrqnX!`EU;ZORJweZFAga*YH-s z$!KK6dDg@iVhem~b~ak`#L+%78uGfPtXzjuXygZca!2h8 zR}~j_*5t?S3d_{|wif9?P+nWDbRtNv)0xD2(X7!~GzL$u+NiQxeA>3X3(q{OHmA5G zyR-e$kpn?>bIaI?ptV`hA2LkNXnDBVBMAHX=@P`|IPT!dL45oa1rmBt3$waswKdO$ z5$?$f*oAzU+?8ieN!&?`s){^Q>)v5!3ksmmH-L-t5IJjNbijH#&51K62w({N)SwfS z3o$42)aOpi zNl(YocbUPZGiz0Knf%OJAg(?0%COBzI zj$MDT+N>8f8nBST;4Q26N9KjWy*SCnEg;0{xkku@=a`dhDe*ng3K==J_BD1G(;vTs z2`25+TKH37mwwGY%{Rf+K0N}@$mX1-;BK;I?o?Fh5fqjoZH~(C?*ymSiuNW6U2e=`$Sgi9%l%Gh@!>WiDA=QaH0tlCuR5%u{tvNIPG%`oLuct(e71y>h8~q1pL7al}4i$vZ-2=hGSkzojOzJb+}{R%D5|_ z7BwXv$oR-kQ*2cm9ZcMmUyEuN8%7b@?m$3cS;%wc`K;dn^4@{G}E4 zOQK{O1pBX#9s6_&%99@@&%!U853%xgSdfn5{lb5TS%f8d6`pJm?q?c6yG0#{1rX74 zLUsf!js{nEw`=xnc5T=%+}M!SJu7SW?5tVcSq;N0*k2FC@5kfKifrz!_rX8Jexq+T z-bY%8U-{aLccgA0Kg9bnzkr+=&|-*4T3TwAO6a>P)7@&*aP``MyuSACvDN%6D2QXA-Z_6{O3V zH{^S&#Xi!(9O8A`zN(F zR+yQsH)~Y2eq+F-wrk88qM}4?R+$VUPQ>c6EX9R}G@Zt!4ft@DNEA&LrCyb*RG4&1 zjgrl!5b~iJ+M%@Hb9FjaHM-A_jK-qs(zufFV4fKj4K3SJm0g;iS)9p^MNYD0s~oya zr`=;oPxtr?-rO9I@#tPtX=k`Nnyb>JY(bH3vD(y|98P_f)skg|*?y>R zC4;9lS%$ZO!uWkpN&k?wk1Cvo$9}|&CqDm)`@Q^ZoNfRdaXH69RSEMe&MZvt43^dB zrCBG}M5Y&=8Y)V;Af?0ZQedfR&nsr@4Tfx2(Cy9)P zd=J81Y*zmeS727KLa`70?4pOrQufzWIV9rE@H6g1R*g66 z-X^zA@0@&5Ix?C3q)PSS!5Be9}!`n5nY1A{~kZw{S-l6HoMFZ`5!1wpCRu zD68m=4_)0>cE+ALk%3rkV5ShwiFJmW7gxA!KXBL<-@bmyRxIbLAs&T3sR2 z>Rpbr3FXv^_qZ4?!@wmV0&aMH>Lh>VxRmPOIZ|H4Xb!GqIJ*lt!`rZ!12g!1>~*Ph zHIpeU8x`(a@}q(GJ|8?h=NVsXRlMYkZaQ@K9o@q5u0va!<`pM4tz@*~{ct$1VQ>PT zwHzLk)ISce+=_C_>d}8=Jm@n$c&9AIxAfmQ8vG;1t`eilJ(_ushvb|gNeW(4P)y-M3dQrC-b8=TVZA(lqDn%D+Gmj*ZVX>K^5YWrom5C zii4usUYJ8ax3zQ?Bwhu>ZLv-1_B=Kmn!YB=AWoX;=fd$32nO*R4L~m>fkZr?Ex2#+PvZiSDFU$h};B{zwgCSB#jZ}~=Imv^qZ zVg9@y?rh#(TJN7z(b(zpPl;H9hTZhm#BN*0%-hb{blZYV`?WS(&9a)wiz}4kCd{6X zBQoi98|N?s-n*AL-p9rtDbJzhl$mlE8FQN)guWtB$M8FH{84?1@gZx$?v+SkV zF!wRWxIL-j&d_L-9$0+-EbXFDOFqsI$L1CJyF3li>KVCnwVsl!qN_@NZ!F zbJh=ig6aM|8h210(Nh((u)~aMi<2HqT(V#R9a^wpqnF<0*|^b@nCoS0*^5Tc@fCI# z!!-7XSq#&7Lg*u-<<(Ve`Z>rcAe9&47o`}*q;j^xr1CaM zgVA!f!lZKc{wXg9-yi0DFO?7CFJ_|ayUIChbL$?`|Liy`B>Ay z+k9p6Ha+&c2gKqfc8yQa9F`9?t z?jaecd3~)0XtfU~<(V>x$Mg^1csU0?{`YX=OD@{J zf?T5t;RyZQ*)%yT@i=-iI~y7^XfvB1=J9Wik4GhZ;MZapttXU2y5A_31M3(Q_~7+{ z_3!glF4bqcfYi^RXy^MHlNKKczXfl&*}jGl5gbIalO$a!?bGrRAeHbz;hepT7w(-~ z)N%dxo!7M|9I(WzCS^BvMH+MH%Gn2&M<;iB978<&JaOB#DNg_mG`VifUG zjFQ36gTgu)+0Y!GQAy|=qZ(MC3!IfTHJu)M@3+5wFR{Wg#kKx>9aFCCcUR__vf{;g z)mC$j@LQFVscYX?s21PR-+$Ypn{uL^l^9v4rGq*H<0n2pjz8#1Y>hJMc4|3vwTs4- zvt5warv-?1p00iyezE&>nDh6XsyuR{^1@*;=mqZ1qzT5D8O*qQE(Xan5XT zv7Vich@bj&I!%T+m*GH#u&+%@T0q z9NYI?>^2|FGyU4UnfYd$}#0wm7P*KRwbYSZh3vI%B5&0d8pmkW5Ef1 zOs9mEI_B7&KTs~pOHnr6Wv96yxNFZ^9-P|ft=U?clW%p!D+(LSGFo=bY}-^?TfV@b z={C9Qt0GeazpGi@lx8;{$;mTjm~5_WhqVwtJKnmaBF}z}BUA4%;6!YOGd2x#V%~B$ zpVLe7oFqMRLivauSqcpruwzcVJ~Zenxuhw@T*sE@QpA8USKuGNmU0|x5YiA^G(-Ts z;EB71oV$h6n{Hwp$ZK#2USkkoBp=6KCVE6mj3kxk(tJFZXv;X$$WN{F@HX3>z?JDr zi7~;a!fc5%pNL*?Ao0h{k}Q95i0)?0xnDTGdh2mpk=yHMzdM27$Ptd9H%M!)-@}S! zJ7mfaARKAAaMjf}9IK8M2AxmP?1lTLTzfISKe1$bOKxMB-U|464;=pA%NYEW!!MOH zjf|JWUc7WnIosc*au$BKgVHP zFk5CSD92{%V3DF$K12EaG{$YQm{G7v%s3taYKO(()0&I3sypqW(ha#~8Mlu`$r+hA zy|7-XnN*t@Z}4XL;$b>}B9a)DrP+Np0@XqMEeK{GFp@sRsY#kd91*bLWo3jtZNftz z{^^6?_2B&rD&VEAUiPaR*dhxFu_V_cQ$JvZn>vAz*6xC8Uq7%Mg zY~;Dfpg#q<_s=bYMyC;kG;NyD`se!|=@hj(t)MWNP0C3R)GKlNLKKZgMHd^2#?p$T z5vw&4Elx{3i-E5<1mjiK+|I6+bZSie)6qJ!6EEkrTj?q`I}Qiufxk#RKqcS6gmU~v z5lLs0${DSBee5?cbJ|U;-$r8`OVD+N7coyATSbVZ*l?DDsbVj_b@4^N|Jz%y?K%6z zyKlXPKLoPm%{LQQGt4l&9zqKQmYeV7!#Gy-c5=X^9z$5Aa7%e#W5Z zbvg7OAVXiT9-(9S zRs3Aa%R)}#Ia>Zwqq29}5Y}!3@M{ZK>B$s;cPBHCy);D4fB@^2+I@XKh)WpXZrb6sz3P z7%9ok^aX|R>Sc**rsfn_0$ueB<|#!@L7vmPD>E~3Oz`ApXO;R&*c_y>6M*S6c@96} zQj(X8Ho06y4IQV}R#8`&QLs>0N_XO~WU})|;A$Vve8<9cr#0G8ZQpIFtTewQd}b^v z3E<}d!qIf}gTDv46n~E}fwNlwNz5J{Pe_i8R{%y58Fb<~z)TP^o>uhE_D2VNm&^#2 zoilK|$%q<UHQyV z{f4Ov3SFLAfoR1*eZ_3AZ)W1Ncu{Vqv%pC$z6!-1HlGlVZ=O4+u-2M!p~G%3a{G!b zfmsa=(+lv9Y+;tedQN7B)0vT;I7a>MtaN+6v*eLLFrzTzbl7X-D^jAsOCvKsekPl_ z4>QllU)?M~Y^F}0`Tr^GOTgnU&bxPJwOVOc+WWrm`;fG&U0tjDwruP0A=$EQ3tM)K zZIEs37!1b97shsNbA>=5v?MVk1d_BQ?NbmRv;_Pm{pvuPq&#h&(l#k1&Bq7E7=l*c zZ|1)%$%Z@;toHBG{O5S*o%f!15U_|jWmAG+vSi+Ze4^!sD+U^|U_=*WpZ?CxwY~M5 z)mG_is#gDFA6Hpxmo<&-YjQS5b#`THt4g6Mi}`F3dArVOQQPb``etY#)4a|*ZfW!N z9gjU!qqeH*Hr7Xb8x6+ltgB`qmR}(=Tb0hBvYb3`vKj&^Rm@=0b8J%KBjdo5O+0}~!9(58Wy zXbG|lv3MD&;3pG&WqL69Kin{4l{nG=%#si~uCz{fCcHI5Krrh+`Zd!Y0Yc_B3*+ZTZiA+%* zN}0%Xa(!RSVKcjTjkSL%rYwH`J#8X$q)RefSxL&Z#GL4;z+CiyC0X5vV%;wFH6=SC{nH8Imhu5UEm<2o7 zkEt`!8_J_p-(9z6cWQl8L$kiZ@!;%oi&y21d3;_vV9yvm$qV;Q9BB(1GxS>i>*kt} zwY8gaW<9~3+jUV#uCBhx z5z}7uE^9dG@T=}u`5lMq`@JVi_2#3wOj&7prv8Mu zDs+CZVVRfpRgb>nm_4M_E+$LjL*iGijE!9sA3rp=2Q`eSAs1?_1ytA>4CC0?l`AoN z_Rwdyvo;vL{=Qhtqs%xJ38r|YMJ9{l>NUBIn@hGy4SuV^XO)Vznc?{6RW@t6%w~b< zsF6;HUK8UK3aOljB#PF2xS@9o#qCpA1;Jcc#D{<{+8dAcMWcPOcyE;KYF+O2E^lpK z;q$S3&?CSl%DEo%8*%R(Bic8Z8bRer^QuFg12d~q>w|TkMwdNiGRN%U5x-}Rtm&@F zt%q|4-5I6EkxN!JSS`k{7_1C~S?f9qzD869zV=lMQ0RcN8@C(MfaLD>2PTg2v;)T9S ziN+<2K*i=)90Bx?Z!FZN;HI)UuV{OAYF!@Yb3nqw8fI`&X~Jt*c_F zh+hR5w?RnOBMJlOXa%`|OsV2Y^&vnQaK1~Mj4Np#0-{=>^s&??3Qa0w1oW0b*}|X< z+7p?-BDzYe1Rvgkq5}_bDx~$`FpK1(;l{>rG*|o{N=G8;>R60?KNX3jYGN^9o^Ad% zWZ1m`Uy0}d55<7E@@cuvr9LUss7k(hf(2qEwoZmHm-)Y#_ll9v)YB^EsipPVdxMdI9W5<8 z1|r384(qG54V2Z$=se;8K+-U515eH*9O_cgEOP|#zyQqS_p5K2Ugyt*6eT5+m(PWz zm7%&&B0&5#o_Jh#;q=+_dcRTM?|opdtlQu-JlxQvZ0m0C?rITssyIQfVL!OB&QkI^ z{ItS)U!`X-1}8gld*c`i7Ict5E-GI)mR}F=*taf_4dXz1#q{*c0|SpmrHWv>DeNXu zuOsA@9Y1{HZoSV~(I1!W@9o_`_ZlBlRdq#eeRXrB9rJf02lWZ8oDv?$;`tg50mkW; z`i$R94<{1C>Dr-0VyM>c_1f_vyW>OY^iVR%e#iHDY<8c|ZiBI$B0gt|%+q-Rhe0g` z-U%GeE_vQn<@MCmc)V5bRr%P1uZn&z;Nm6Eaq@q!IpcAsBbjs*zKL^Y=AS~*q|cX(hRB`8C$wfke#!_| z!iSZp#hww2K*mLafjCM=Q-Vl#!nvw9%SF#XP%KD2#t&L2BFUQ*&69PR&28hkEupA? zDAYZf-mzzV+-{SZ4dWV9rQD=5P`&06y}9HLP0dhiZZKBT)hV{9kE^sL2ldIm#-Zto z;ddkkZKXybMOIn4x}rj>kYY!N=tj|p;xe{NN4PiwkOF7VgmbxYq_I(4=1En1z11lX z;Pxo$PKnC^xA6Sl1x#xNCo*#X?-%G8OlU9`)CbZybX!aQTQt{@UXuuI>|Hf8&~?YC zCtmWD)(7R_*43+{bvgr-gT}iix7^))Pb6Uua_OIOiB`@fq72WWyFRnP4;IV8J*tZQ zYiX%vNh!1>!rDNttNDexoTKoTp;un~!j`+?;1G-47)J!S?F3a?)GSID-DjK?!)?Z9 zEiTG&fgltuh<1!;z)Zm;yI{}FO|GoZ(;lnayKddytf$fMsCG2(Qbq%@h{aP`Y496X znY>DiN@3G@>05#2jnTF$jW*SltY4q5S=Uh0S*6vb+QSV4!MXPxevjSYvq;P2HV?VQ zW>Q*>EMD6}p~|Cw968q``v9|l<+3Ddw zhOQK&dXCzu8Y6|IIRZ)|!6#1M&#TD$VD2ZGmz@wfMat=6f!o@A@5^U&&*hkBZKw7ZkevJ!>MG*;iO3~Em~yh^3d z9`ngy9uuitnvSt#V(QR}bw_&|riVvvtuNOosI|t_ur4k$$@8OI;ihPJaEZ(!I%KP5=aB_SulMwB@f2N{0+8@!y0j@#b5t6 zA*YX&x@vuaR-2_aw(`)hdZ2}nmR{A0yXadw^@WN`h1qM^B9==_^~t$GJk-TiP339cZyTuCDi|&C(UnO&PBpaP2ouHGxCpfCkhe_ z;~$~<4S18C;bqb`6lNGg1yTo2(zf(X63y4pgY4CsXcUluR0&OLj$5LcYXOJ$*#5?A z1!+*PPmu@q&-N`3jC4f117U?%XVC2@U(HXEuTCV(d)GxPD-R*vt3!#_5`e1>GXTdf z`YbSvC9VOevQi}c#j!vbVLF>88(Qz4Y@P}QcEo(umc~Xak~Lb%!ylHb+~lpyRR7>D z^*Zh2YMU->4=3cMUUKls2{DWUl=*~dBYKjt8El1HrxtcOxY_-P$zg$WEaa+j&mHSVfI&~J-@4*qd*JC0En{GdAjqSQNz|X@n)PYkNkrsq)<;B5s z?QKDm2hkU_cytDm7x=KD$55j9dh5)Vj;UnLy0(UTvA9&*T|c_})+a4KbtqzVY0H&< znu@zS=l*%cj;uoSgkB#B$trz1ck;sN)Av8%YDxCCDD5$aF`9D;v7c_=p}KI%wHzI&ue~j$EouluX>xUv`-X7UVJ%fRbr(D0}l*DEFRkT(!Ixy-TM+T zzj*5S@l!7n=kNNSex~=gmBJU^_2b=R#dq5TvyepiqQ_pm@A&ciUd;dI#Z$+Q;bqNl zd!Ko_?{}5Lm%w#`xj@G{H@_0h(*;#rzx5{AmC?jLN$n%}Z)`ga3KSV?5 z7e9169wCgj@j%1t8ZM#*ld2J+Lgah)N~l~D&!Y8oq4j|QdaZZA&e&;W{A{cz|N0y{ z7|8xF+OVJ9b_V^L`nfHb`7;$oYp`_TiTcZ8^;z9bih#f3h5umB+M5)9f8}4(E0v*G zNU#yh79Sz8^+RXx;W)5z9wSFD&6rF%AQmN<~lg;K|L+O#lQet8LzIk}1uu4qYVs#;B&+o!FLH-R9p)`!%nWr#? z{$^-$Ff67|<0Hv$BY{GjamoBlLTmx-i!FMvY1tht>P)6zePnZn=^WrC# zS^RY&l1T`KS;{8T#wMN0sxo?KgX=mPBf@-(QU^`UJp(aA{Ff;YpE~qd)-u7EVGa3Q zyGz@;p$SG?FtGc9GqCm)@GuK=gci;nj(`;Dr^lmHs`qn*gEe;j0$UCD0he+=j|_{7F&_FNH394YhD9lahW97zP;+> z^QZ0*qH)Oc7zOl)LNtyVv_icn*Lh^NuIKPSxDWP|^Z9o?2bbkv#{k$U#23Lwf`AG1 zjpCjZ9t7IDL|kNN{OEse+%oHNCw#LCo5Af@%$~DJl!?}GZl$@@GD1_EZpL1Xw^R@H z=kp{E7j8jnL^ji~&mRIFt$^L>yYw>jmP_*iKIcMSTEt}P`CPk1_rRvz%4X5VDV;#G z$i?yFN7o%cI(p=uv7@wg?D(;DM`za0%#7btSltOOhh9IE;_%61Yj)1Mycz%OIg3%kUPoZg8;> zq&2UohT)n*yug;>P+D||L1=MMcyk2v35?7MN<-GV(c1?N4LW_q*0eIAr=^v?Ot@*R zDz|4v-RAyq{u7Vi>oK^rWoj!qKX7DAw~vY+DJij3b%acQL*`5UgVRlc6?d#oq(VvL zxYq`pERKSYpV`3+^V&;a;Uk6h5ZvVwtOHJrl*yA)Z*Q(4eXY!Lp@OYl!Jr#x`h zH{%=j-s9bUT)FSGcg8n_&#n1~hLl6(%I$bT|4^%fxewxYZ5059?-R|}zjeIxU=%It@59?S|2B14i z1TC`Y8d>nm{q6OBxy&ZNZ?bvtP!Fb++2AwP_vD|81~4l=H!|`C{y4YC?ddo&j;T7G zI;-+*Z_mPfD+DhbFg^rI$W9yTKnVrci{MFQ;xJ1C^3-)<1j{_m%^Kwr#mVf702=cb zU}N1aGGjOHo!{{VP?Lhb0Zv~4&6Nw*fML!R1ZZBGVss^pUq1$3L1qCpOv91DBwUz( z$&de@l1jaNXv;_6A)96CReQ8rA&vfj zQ!}#Ih0P!^Lci1Kw?c?+VT%k9G8si$G#UUq$@IkFvP~NXtCuJK&15zkjb^inTGwux zTs^umHq;PdKTuFOCjE%+rfble2ihm&HLS^n$TKv|v`YgE`eOc>tx8_eEeccd!6mzJC<*U1~J{+WtTm1$++gq9e_k93rmwWgc=xAx!r zfUF!j<9xe&^DlfJo_DH~BbU zN$;NZjjt$Eu}kmVWjLIaeY{>|DQT7rW8QH$MsK<=>cu z!2CGO&CK-(Zvv@Wb2U$No{ReP!FMgX1)B0|&Xd6=puaNFIz^^lcwq+rlfRgG{`r~jGt4RKrrfvMg{{A! zg>o&OX{uiF;@MUJgxpiqK2z%(1%=)_(Cc=U`yKu@)t%Y9`fj=1 zn2Ojviih+~QD(A;Hj#j%I%d#2EtNWx#d6H$pBxOdX02L_NoD?4(Ud{4;v?h}>=P%C z-!JqfkYkW3VcA(mb)2}sZ9@yRg?xz(z$q&X;Ld&wARTtNmkxjk128LeCad|FGr$H= zXVF^BN?`!ZAOZ+9!qadWv{tZiAr#Av&%3(gI`R;HA&@FhDEqK=wa@P(S48JQ?N5v3 zbByZB;EeuvoadD>*{h9thJu2SB0P|PFV`w1-;|cy+^XrtNBTy5YBI!6`Yt@Mv1riR zySz16Ie2R{&;-vZ|4$N^rehiPR7a^QES}7 z;LlomkIR>k_PI82E9b3TgfwL6_Ihc1;&h~&v$`r#)#7{xF0o3@BPKo4 zMrjOMZM-EuG;A{7p*2+Cz_X4O2)wnhqWlyV^t-}}@=-CFl$94(6tt0zb)nR_lT%x) z^I|DZaNw3(wwuzRZU2Di@C=I?18sw}Ab`$z-DH@n3QU<(m+Nh0S!O>&SODi?rKRbV z-Q(T1v1Y{OhZlv zklwV>-!f#<>{OdI8oAM|FxJ$$YU*^lreMdoUVlJoQdsl~BnvUbn{z7QhP2GA)!6Nd zQWKjA_f#+Vyo>vDsU1M@TMOjiwIU7X9&Em6Nn6qFjTML1NW<8xrOeNLyV z@n!mB2CjewTA+YUuc)8w3zKqZ0D1M?nsT*`ByzX!9I29sA1;xYlN}MJfNk#|+|%S= zzJF!1CY&&OnF0ob0})Ap4?o0+n5+fPPkxxc`ZSRhLV)mhXo|ERG5g*nGzPvJ#%dW; zaU)ASV{LZ~5|Yp0?ae~Z!Rx_Hs#(~#b|PhqAX8mikue`e5}QB+z(=@Us;)LQ<; zBPwgiU3H>iU?PrwEEvbL^ear(-MScj$Qp(lu03 zeU%!|$g=-j(X{WDKl{0r$EKhy+||i!6@LkQWgc%Ar)C!5Gx}I4cR-q8SRlxh*C63z zuNLHD2$UdBOrT_FEG!MqdNTekh)NkcTD!i{QyWxB5u7qRTZ+uD;d0 zEY3<|fKaKit7KM-{B7}nUkqiHMxWvQskY9hCPjOfGsjkd;e;D2kP!N2mqjqpGTFRf zodUfmCi4XRuYc{mZZ10*(Bqu{XDRWzMES_z(#eA;%hjHRrV+gWampOOH4rqih_E8onyzi5Xc;4g)E*m@+0L);gnB zZ8#Rzmg^;BrR`OQSTw9D*Ok)og#_Ov@STGzaJb8}LHw%f2Cxz zcF-=HmpLHG_WVyXbwT(fL4}337`6Qt!$eSF!EVFLxle+XBl>qUqqo=BhFySLbA(c6 z7ZB?D&7ZH!wBfrrj^EhE%FtqM=D!u%xCCzvna|JnkgM~vZ0~n5J?m<|j9mMhFY}S8 z7z~QzMx5!;V(To8e-2BZqe>6dGr47h-wz!gLfI9Oo#z*u~f=31j2i<UWEP zteBw$*gB0F%E2v#aD8Uo_5R@LU#?p7%V+=Umm4ZJ$8xuGiU4k2MHof|`xnr`=5>UvUYFE%w4*PljRTv87I=hTC z&;3=$ctRq6h?ZEwCJDw`=z-1o#R6u2gU#WFa^N7(jV+9a&6w4OZR!tfUZ9?|8#^4k zu?>sEc*3Fz29AJp2L6dxzq@h&{{7$k@|VB7;n`QoTUYZ>ZL6Q!zJ02G8yWwC{<=4@ zmca$cJD5LAVA$VUMkdhMJNe9i{epb&?O*RAkqi6tzhW?FHUEc8sIM00v6ruf&;pwu zD~Fu(NwM}j+$+Fq!@oC;zfu+rPJ^H)Dv^s(;liPQH2HJZY5=9rSw_)Kc+;rL!tBsgD?~j(J}aD`rz*Y?iAi&vHyJ~rtj<1DTCY`uK}_Mv8VUZ%bcHsH3EJbFXYU35DnVe(z^x?j6mY<;*!} z&YWegLMS0bkBfqknH`;7_ECEeAwn%7bji$x-910N_OF){;?E+){Qa3dOWXcuTgAT# z$t@#9t?KRxgfF^Cd>3_JK`-limb5N8TK9TCzF&p!EgQD?4gct+nmW`)eMNFp-{>$K z4bRQEXKfnTd-8+(-Y+4f?IuEYy|B5zZ=?CwKfDLe*Wr15GYXV>!f)}t5#P%;Zy(!p z_3!`rGeS(&=(BoYXhYu%zy0_FLd1)auG-$WXPEw(W}|)~>N^Mfw)cNl;qnmTSdRKP z4G)cuU9@At6NHq0g*oI5kMs|3R$Y7!>gS@q`XQP+=WfM!@RiV1isa!==QfOsbG z!!R}VC$d_Yl_%MZvEQ9=CjTrxbxa-Bnza(hSrg$O4X({={G5mkUxd6k{z5Cezgc_f z&(XF;d`h83T@~s|f2{W`{>{va*k-E2b3KQO5`or|jcCRpq+tOV zA_`$1?x-zXC}8$vu5_nsNSH1_X^v7*C>4UBI6%k-LY$}~{mty|o=L7J#Ph5)H-(2j zfu6t1o(k2%VceX62%{1mE5qOSk+)&2T$JVkDoRYmjH`_l;97(aB_+g-tB1siNNVw+ zq#hqi8b~8bnn?@t?W6(jFDZ)?<4zh zJslsBoI%b+p5c*@dyy_g8bn%yGzX~=kgACfP!K+{o{wWBt5CzrM{w}Ii+F#<=qU>@ zdpTq)$V##n@UY%Tsg9ax0rk?2_@0Y4U&QpIa+*La!$?}%6TwXmmALAntNmJi@ERReyXk3ZqeSN{e><|w@`PL?r(Xnycv0O z^Y-WcJn!kexAQ*NEA)1KwSJ9$kN$xEPW>PBM-5s-%+PGO%hpMTn{_5ypS{ebu{AqErL{nldag_{}oLX{y$*WGK)8H&} zRyc>8r#jDbe%E=O^KR$QoKHJHF4dP7mX?=BOIu3kl`b#cSbAgWU8P4{Mpv<`(iL~L zxlVG8x=wdphJ6Qp zxBH&(9WM8kpH#l3{Fd?;%fGA;D*P3vR~)RkrsDaE*DBtr__Si8GP}}TIas;B@`B2P zl{ZwrQdM5nP<2t&@2ZYg8>(xow^v_T{io{ps*l!?nx!?{Yj)LKP;*PoFKT{Q^Pb=2 z@9+=$cl)pOU+2Hg|A7Aq|8xFV{C^JU12uu}z^cHez-VB9;KIN)fu96E349r(!Ghq7 z;N0M{;QHY0!3Trygd8DnC=_Z89Sl7bdOY-8=#|i)L&azOJ)wVcqJwO?4x6r`KIn zcc|_sbzjsA^}71}dQW|G{ZRd>_2<@KUVnf6f7icM|3>`>_5Wz7YUpTK-LM7gl1XKe zuTLiT**aAy-p73qa!X14$Y~*kZX`b>x0Acc{p2C?2vu_l;if)XMME?}o9Qe%pRS^P zw4d%53WS94GvO)W8R3tjLd+6#MWa|QhIIA1RmJBNUt9c{Q|r__%}$52*y(b5o%PN; zN?k6a=UMDb>`x0Rd_Bgyh1`kp9v}~s|E3yhrNvzS_#r#$Xe*rqIkX0nXscl7UrI3b@qhVa5w3gP> z!}K-!HvNQtN{`Zi(XRxRU=|!gP>2h)!a6pDu%0DhP}n7$g4}6H`-M-$au6=ao{OLo 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�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 Xvia8awzvAedXF~MWX)$92Pd6 literal 0 HcmV?d00001 diff --git a/www/html/Create Room/FONTS/NotoSansThai-ExtraBold.ttf b/www/html/Create Room/FONTS/NotoSansThai-ExtraBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0ed192b5b0beea7ff825eece329669b98f684cc7 GIT binary patch literal 47524 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 aG*whK;Hea2aMjp3lF}-QBHL+4;(q~WaKnB8 literal 0 HcmV?d00001 diff --git a/www/html/Create Room/FONTS/NotoSansThai-SemiBold.ttf b/www/html/Create Room/FONTS/NotoSansThai-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4a04f1ac1059baedf06d049701de7aa3a2a5b6e6 GIT binary patch literal 47552 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

_idle__layer_.png หรือ id__idle_layer_.png */ + function layerUrlCandidatesIdle(id, dir, layerName, frameIndex) { + const enc = encodeURIComponent(id); + const base = BASE + '/img/characters/' + enc + '_' + dir + '_idle'; + const out = []; + function add(u) { + if (out.indexOf(u) === -1) out.push(u); + } + // idle เฟรมเดียว — ชื่อจริงของ char-* คือ *_idle_layer_*.png (ไม่มี frame index) ลองตัวนี้ก่อนกัน 404 รัว + add(base + '_layer_' + layerName + '.png'); + add(base + '_' + frameIndex + '_layer_' + layerName + '.png'); + if (frameIndex !== 0) add(base + '_0_layer_' + layerName + '.png'); + return out; + } + + function shadowUrlCandidates(id, dir, frameIndex) { + const c = layerUrlCandidates(id, dir, 'shadow', frameIndex); + const def = BASE + '/img/default-shadow-' + dir + '.png'; + if (c.indexOf(def) === -1) c.push(def); + return c; + } + + function shadowUrlCandidatesIdle(id, dir, frameIndex) { + const c = layerUrlCandidatesIdle(id, dir, 'shadow', frameIndex); + const def = BASE + '/img/default-shadow-' + dir + '.png'; + if (c.indexOf(def) === -1) c.push(def); + return c; + } + + /** ลองทีละ URL — อย่า set src พร้อมกันหลายรูป (เดิมทำให้ legacy `id_dir_layer_x` โดน 404 ทุกเลเยอร์ทุกทิศทั้งที่มีแค่ `id_dir_0_layer_x`) */ + function resolvePlayLayerImage(urls) { + for (let i = 0; i < urls.length; i++) { + const img = ensurePlayLayerImage(urls[i]); + if (!img.complete) return { status: 'pending' }; + if (img.naturalWidth > 0) return { status: 'ok', img }; + } + return { status: 'missing' }; + } + + function ensurePlayLayerProbesAllDirections(characterId) { + if (!characterId || playLayerAllDirsQueued[characterId]) return; + playLayerAllDirsQueued[characterId] = true; + ['up', 'down', 'left', 'right'].forEach((d) => ensurePlayLayerProbe(characterId, d)); + } + + function playCharAnyDirectionLayered(characterId) { + if (!characterId) return false; + return ['up', 'down', 'left', 'right'].some((d) => playLayerMode[characterId + '|' + d] === 'layered'); + } + + /** รอเฉพาะตอนยังไม่เจอ layered เลย — ถ้ามีทิศหนึ่ง layered แล้ว ไม่ต้องรอทิศอื่น */ + function playCharLayerDiscoveryPending(characterId) { + if (!characterId) return true; + const api = playCharLayerFromApi(characterId); + if (api === true || api === false) return false; + if (playCharAnyDirectionLayered(characterId)) return false; + return ['up', 'down', 'left', 'right'].some((d) => { + const m = playLayerMode[characterId + '|' + d]; + return m === undefined || m === 'pending'; + }); + } + + function ensurePlayLayerProbe(characterId, dir) { + const key = characterId + '|' + dir; + if (playLayerMode[key] !== undefined && playLayerMode[key] !== 'pending') return; + if (playLayerMode[key] === 'pending') return; + playLayerMode[key] = 'pending'; + const cands = layerUrlCandidates(characterId, dir, 'bodyColor', 0); + let ci = 0; + function tryNextProbe() { + if (playLayerMode[key] !== 'pending') return; + if (ci >= cands.length) { + playLayerMode[key] = 'none'; + return; + } + const url = cands[ci++]; + const img = ensurePlayLayerImage(url); + const fin = () => { + if (playLayerMode[key] !== 'pending') return; + if (img.naturalWidth > 0) { + playLayerMode[key] = 'layered'; + return; + } + tryNextProbe(); + }; + img.onload = fin; + img.onerror = fin; + if (img.complete) fin(); + } + tryNextProbe(); + } + + function getCharacterAnimFrameIndex(id, dir, now, isWalking) { + if (!id) return 0; + const key = id + '_' + dir; + const anim = characterAnimations[key]; + if (!anim) return 0; + const phase = walkAnimPhaseIndex(now, isWalking); + const fi = pickLoadedWalkFrameIndex(anim, phase); + return fi >= 0 ? fi : 0; + } + + function tryComposePlayLayersFromFiles(rawImg, id, dir, frameIndex, tint, opts) { + const useIdle = opts && opts.idle; + if (!rawImg || !rawImg.naturalWidth || !rawImg.naturalHeight) return null; + const w = rawImg.naturalWidth, h = rawImg.naturalHeight; + const c = document.createElement('canvas'); + c.width = w; + c.height = h; + const cctx = c.getContext('2d'); + let anyTintColorLayer = false; + let skippedShadowWhilePending = false; + const diskLayers = playCharDiskLayersSet(id); + /* ยืน (opts.idle): ลอง compose จาก *_idle_*_layer_* เหมือนหน้า character upload — รวม char-* ที่อัปโหลด idle เลเยอร์; ถ้าไม่มีไฟล์จะ missing → ไม่มีเลเยอร์ย้อม → getPlayTintedAvatarSource fallback ไป walk compose */ + const idleLayerStripes = !!useIdle; + const resDir = dir; + const resFi = frameIndex; + for (let li = 0; li < PLAY_LAYER_ORDER.length; li++) { + const layerName = PLAY_LAYER_ORDER[li]; + if (diskLayers && layerName !== 'shadow' && !diskLayers.has(layerName)) continue; + /* char-* หันหลัง (up): ไม่มี face ทิศนี้ — ข้ามกันตาหน้าซ้อนหลังหัว; ซ้าย/ขวา/หน้าใช้ face ตามทิศได้ */ + if (layerName === 'face' && isUploadedCharAssetId(id) && dir === 'up') { + continue; + } + let urls; + if (layerName === 'shadow') { + urls = idleLayerStripes + ? shadowUrlCandidatesIdle(id, dir, frameIndex) + : shadowUrlCandidates(id, resDir, resFi); + } else { + urls = idleLayerStripes + ? layerUrlCandidatesIdle(id, dir, layerName, frameIndex) + : layerUrlCandidates(id, resDir, layerName, resFi); + } + const r = resolvePlayLayerImage(urls); + /* เงาโหลดช้า/404 บ่อย — อย่าให้บล็อกทั้งคอมโพส (ไม่งั้นตกไป fallback แถบแนวตั้งแทนเลเยอร์จริง) */ + if (r.status === 'pending' && layerName === 'shadow') { + skippedShadowWhilePending = true; + continue; + } + if (r.status === 'pending') return null; + if (r.status === 'missing') continue; + const tintHex = PLAY_LAYER_TINT_KEY[layerName] ? tint[PLAY_LAYER_TINT_KEY[layerName]] : null; + if (PLAY_LAYER_TINT_KEY[layerName]) anyTintColorLayer = true; + drawPlayTintedLayer(cctx, w, h, r.img, tintHex); + } + /* ถ้าโหลดได้แต่ไม่มีเลเยอร์สีเลย จะเหลือแต่ stroke → ตัวขาว — ใช้ PNG รวมแทน */ + if (!anyTintColorLayer) return null; + return { canvas: c, skipCache: skippedShadowWhilePending }; + } + + function getPlayTintedAvatarSource(rawImg, characterId, dir, timeMs, isWalking, tint) { + if (!tint || !characterId || !rawImg) return rawImg; + ensurePlayCharLayerListFetch(); + ensurePlayLayerProbesAllDirections(characterId); + const dirn = dir || 'down'; + const frameIndexWalk = getCharacterAnimFrameIndex(characterId, dirn, timeMs, isWalking); + const idlePhase = idleAnimPhaseIndex(typeof timeMs === 'number' ? timeMs : Date.now()); + + if (playCharLayerDiscoveryPending(characterId)) return rawImg; + + const apiFlag = playCharLayerFromApi(characterId); + /* เหมือนเดิม: มีเลเยอร์แยกไฟล์เมื่อ API บอก hasLayerFiles หรือ probe เจอ — gen สีทำบน canvas จาก *_layer_* เท่านั้น */ + const charHasLayerFiles = apiFlag === true + || (apiFlag == null && playCharAnyDirectionLayered(characterId)); + + /* มี PNG idle รวม — ใช้เมื่อไม่มีระบบเลเยอร์สี (ถ้ามีเลเยอร์ต้อง compose ไม่ return raw ขาว) */ + if (!charHasLayerFiles && !isWalking && characterIdleSpritesVisible(characterId, dirn)) { + return rawImg; + } + const cacheKeyWalk = [characterId, dirn, frameIndexWalk, tint.head, tint.hair, tint.body, 'ct3'].join('|'); + + if (charHasLayerFiles) { + if (!isWalking) { + const composeFrame = isUploadedCharAssetId(characterId) ? 0 : idlePhase; + const cacheKeyIdle = [characterId, dirn, composeFrame, tint.head, tint.hair, tint.body, 'ct3', 'idleL'].join('|'); + const hitI = playLayerCompositeCache.get(cacheKeyIdle); + if (hitI) return hitI; + const packI = tryComposePlayLayersFromFiles(rawImg, characterId, dirn, composeFrame, tint, { idle: true }); + if (packI && packI.canvas) { + if (!packI.skipCache) playLayerCompositeCache.set(cacheKeyIdle, packI.canvas); + return packI.canvas; + } + } + const hit = playLayerCompositeCache.get(cacheKeyWalk); + if (hit) return hit; + const pack = tryComposePlayLayersFromFiles(rawImg, characterId, dirn, frameIndexWalk, tint, { idle: false }); + if (pack && pack.canvas) { + if (!pack.skipCache) playLayerCompositeCache.set(cacheKeyWalk, pack.canvas); + return pack.canvas; + } + return rawImg; + } + + /* + * ไม่มีไฟล์เลเยอร์ตาม API/probe — แสดง PNG รวม (ไม่ย้อมทั้งตัว: ไม่เหมือนระบบ gen สีแบบเลเยอร์เดิม) + */ + return rawImg; + } + + function getCharacterImg(id, direction) { + if (!id) return null; + const key = id + '_' + (direction || 'down'); + if (characterImages[key]) return characterImages[key]; + const img = new Image(); + img.src = BASE + '/img/characters/' + encodeURIComponent(id) + '_' + (direction || 'down') + '.png'; + characterImages[key] = img; + return img; + } + + function getCharacterFrame(id, direction, now, isWalking) { + if (!id) return null; + const dir = direction || 'down'; + const t = typeof now === 'number' ? now : Date.now(); + if (!isWalking) { + const idleAnim = ensureCharacterIdleAnim(id, dir); + let idlePhase = idleAnimPhaseIndex(t); + if (isUploadedCharAssetId(id)) idlePhase = 0; + const idleFi = pickLoadedWalkFrameIndex(idleAnim, idlePhase); + if (idleFi >= 0) return idleAnim.frames[idleFi]; + const idfb = idleAnim.fallback; + if (idfb && idfb.complete && idfb.naturalWidth) return idfb; + } + const key = id + '_' + dir; + let anim = characterAnimations[key]; + if (!anim) { + anim = { frames: [], fallback: null }; + characterAnimations[key] = anim; + for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) { + const img = new Image(); + img.src = BASE + '/img/characters/' + encodeURIComponent(id) + '_' + dir + '_' + i + '.png'; + anim.frames.push(img); + } + anim.fallback = getCharacterImg(id, dir); + } + const phase = walkAnimPhaseIndex(now, isWalking); + const fi = pickLoadedWalkFrameIndex(anim, phase); + if (fi >= 0) return anim.frames[fi]; + const fb = anim.fallback; + if (fb && fb.complete && fb.naturalWidth) return fb; + return null; + } + + function getAvatarImg(characterId, direction, now, isWalking) { + const img = characterId ? getCharacterFrame(characterId, direction, now, isWalking) : null; + if (img) return img; + return defaultAvatarImg; + } + function getStoredCharacterId() { + try { + const v = localStorage.getItem('gameCharacterId'); + if (v) return v; + } catch (e) { /* ignore */ } + if (firstCharacterDefaultResolved === null) return LEGACY_PLACEHOLDER_CHARACTER_ID; + return firstCharacterDefaultResolved || ''; + } + function getPlayCharacterId() { + if (forceDefaultCharacter) { + if (firstCharacterDefaultResolved === null) return LEGACY_PLACEHOLDER_CHARACTER_ID; + return firstCharacterDefaultResolved || ''; + } + return getStoredCharacterId(); + } + + /** บอทพรีวิว: เลือกรหัสคนละตัวจาก roster (ไม่ซ้ำผู้เล่นถ้ามีตัวเลือก) */ + function pickPreviewBotCharacterId(botSlotIndex) { + const idx = Math.max(0, Math.floor(Number(botSlotIndex)) || 0); + const roster = playCharacterIdRoster; + const humanId = String(getPlayCharacterId() || ''); + if (!roster.length) return humanId || LEGACY_PLACEHOLDER_CHARACTER_ID; + const alts = roster.filter((id) => id !== humanId); + const pool = alts.length ? alts : roster.slice(); + return pool[idx % pool.length] || humanId || roster[0]; + } + const MOVE_SPEED = 0.15; + /** quiz_carry — ค่าเริ่มเมื่อไม่มี override จาก Admin (รหัสฉากตรงกันใน quiz-settings.json) */ + const QUIZ_CARRY_WALK_SPEED_MULT = 1.42; + /** ค่าที่ใช้จริงต่อเฟรม — อัปเดตจาก /api/quiz-settings + join snap */ + let quizCarryWalkSpeedMultActive = QUIZ_CARRY_WALK_SPEED_MULT; + const PATH_ARRIVE_THRESH = 0.15; + + function clampQuizCarryWalkSpeedMultClient(n, def) { + const v = Number(n); + if (!Number.isFinite(v)) return def; + return Math.round(Math.max(0.5, Math.min(3, v)) * 100) / 100; + } + + function resolveQuizCarryWalkSpeedMultFromSettingsObj(s) { + const base = QUIZ_CARRY_WALK_SPEED_MULT; + if (!s || typeof s !== 'object') return base; + const forId = String(s.carryWalkSpeedMultForMapId ?? '').trim(); + const mult = Number(s.carryWalkSpeedMult); + const mapId = (currentPlayMapId() || '').trim(); + if (forId && mapId && forId === mapId && Number.isFinite(mult)) { + return clampQuizCarryWalkSpeedMultClient(mult, base); + } + return base; + } + + function applyQuizCarryWalkSpeedFromSettingsObj(s) { + if (!isQuizCarry()) { + quizCarryWalkSpeedMultActive = QUIZ_CARRY_WALK_SPEED_MULT; + return; + } + quizCarryWalkSpeedMultActive = resolveQuizCarryWalkSpeedMultFromSettingsObj(s); + } + + function moveSpeedTilesThisFrameForWalk() { + return MOVE_SPEED * (isQuizCarry() ? quizCarryWalkSpeedMultActive : 1); + } + + function isMovementKey(code) { + return ['KeyW','KeyA','KeyS','KeyD','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].indexOf(code) !== -1; + } + function isChatFocused() { + return false; + } + let zoom = 1.4; + let froggerScore = 0; + let lastFroggerKey = 0; + let gauntletObstacles = []; + /** อินเทอร์โพเลตการวาด obstacle ระหว่างแพ็กเก็ต sync (~220ms) */ + let gauntletObsRenderPrev = []; + let gauntletObsRenderNext = []; + let gauntletObsBlendT0 = 0; + let meGauntletJumpTicks = 0; + /** ค่าที่ใช้วาดการยกตัว (เลอร์ปจาก meGauntletJumpTicks ให้โค้งกระโดดไม่กระตุก) */ + let meGauntletJumpVis = 0; + /** ซิงก์จากเซิร์ฟเวอร์ (gauntlet-sync / GET /api/game-timing) */ + let gauntletRuntimeTickMs = 220; + let gauntletRuntimeJumpTicks = 16; + /** Stack: รอบสวิงต่อวินาที — จาก GET /api/game-timing · ค่าน้อย = สวิงช้า */ + let playStackSwingHz = 0.55; + /** Stack: ความกว้างบล็อก (tile) — null = คำนวณจากโซนลงบนแผนที่ */ + let playStackBlockWidthTiles = null; + /** ภารกิจ Tower (mnn93hpi): จำกัดเวลารอบ (วินาที) — จาก game-timing.json */ + let playStackTowerMissionTimeSec = 90; + /** ภารกิจ Tower: ทีมพลาดได้กี่ครั้งก่อนเกมจบ — จาก game-timing.json */ + let playStackTeamMissesMax = 3; + /** ภารกิจ Tower: ชั้นสำเร็จกี่ชั้น (ฐาน) = Progress 100% — แต่ละชั้น +100/N %; คอมโบ ×2 ของชั้นนั้น */ + let playStackTowerProgressBlocks = 50; + /** รูปบล็อก Stack ต่อที่นั่ง 1–6 (game-timing) — ว่าง = วาดสีเดิม */ + let playStackBlockNormalUrls = ['', '', '', '', '', '']; + let playStackBlockHeavyUrls = ['', '', '', '', '', '']; + /** 0–100: โอกาสใช้สไปรต์ “ใหญ่” ต่อการปล่อย (ถ้ามี URL ใหญ่ช่องนั้น) */ + let playStackHeavyBlockPercent = 35; + /** กระโดดให้รอด: ความสูงกระโดด = ทวีคูณ × ความสูงตัว (ch×tile) — จาก GET /api/game-timing */ + let playJumpSurviveJumpHeightMult = 1.5; + /** จำกัดเวลารอบภารกิจกระโดดขึ้นแท่น (วินาที) — จาก GET /api/game-timing; 0 = ใช้ค่าเริ่ม 60 ในเกม */ + let playJumpSurviveMissionTimeSec = 0; + /** รูปแพลตฟอร์ม jump_survive ช่อง 1–3 จาก game-timing — url ว่าง = วาด cyan; w/h 0 = ใช้ขนาดไทล์ */ + let playJumpSurvivePlatformTiles = [ + { url: '', w: 0, h: 0 }, + { url: '', w: 0, h: 0 }, + { url: '', w: 0, h: 0 }, + ]; + /** ยิงยานอวกาศ (space_shooter): เวลารอบจาก game-timing; 0 = ใช้ค่าเริ่ม 90 ในเกมถ้าแมปไม่ทับ */ + let playSpaceShooterMissionTimeSec = 0; + /** รูปยานช่อง 1–6 จาก game-timing (spawn slot); ว่าง = วาดยานเวกเตอร์ */ + let playSpaceShooterShipImageUrls = ['', '', '', '', '', '']; + /** อุกาบาต: [0]=ตก, [1..]=แตก — จาก game-timing */ + let playSpaceShooterAsteroidSpriteUrls = []; + let playSpaceShooterAsteroidExplodeFrameMs = 70; + /** ระยะห่างเกิดอุกาบาต (ms) จาก game-timing — แมป spaceShooterAsteroidIntervalMs ≥200 ทับ */ + let playSpaceShooterAsteroidIntervalMs = 1040; + /** ทับยานเมื่อชนอุกาบาต ครั้งที่ 1–3 (PNG โปร่ง) — ภารกิจ Violent Crime */ + let playSpaceShooterShipDamageOverlayUrls = ['', '', '']; + /** balloon_boss / Mega Virus — จาก game-timing */ + let playBalloonBossMissionTimeSec = 0; + let playBalloonBossBossImageUrl = ''; + let playBalloonBossPlayerBalloonImageUrls = ['', '', '', '', '', '']; + let playBalloonBossPlayerBalloonFallbackUrl = ''; + /** 0 = ไม่จำกัด — จาก game-timing / gauntlet-sync (พรมแดง Gauntlet เท่านั้น) */ + let gauntletRuntimeTimeLimitSec = 0; + /** เวลาสิ้นสุดรอบ (epoch ms) จากเซิร์ฟเวอร์ — null = ไม่จับเวลา */ + let gauntletEndsAtMs = null; + /** Crown (mno9kb07): เซิร์ฟยังไม่ปล่อยรัน — หยุด tick / เวลา */ + let gauntletCrownRunHeldRemote = false; + /** null | 'howto' | 'countdown' | 'live' — พรีรันก่อน GO */ + let gauntletCrownPregamePhase = null; + let gauntletCrownLobbyReadyMap = {}; + let gauntletCrownCountdownTimer = null; + let lastGauntletJumpKey = 0; + /** รูป lane: หลาย URL สุ่มตาม id คงที่ · laser: แยกบน/ล่าง/เส้น + สี/ความหนา */ + let gauntletLaneImageUrls = []; + let gauntletLaserTopUrl = ''; + let gauntletLaserBottomUrl = ''; + let gauntletLaserLineUrl = ''; + let gauntletLaserFillColor = 'rgba(140,230,255,0.42)'; + let gauntletLaserStrokeColor = 'rgba(255,220,255,0.9)'; + let gauntletLaserLineWidthPx = 2; + const gauntletAssetImageCache = new Map(); + + function encodeSpacesInUrlPath(t) { + const s = String(t || ''); + const q = s.indexOf('?'); + const pathPart = q === -1 ? s : s.slice(0, q); + const query = q === -1 ? '' : s.slice(q); + return pathPart.replace(/ /g, '%20') + query; + } + + function decodeAssetUrlPercentRuns(t) { + let s = String(t || ''); + const q = s.indexOf('?'); + let base = q === -1 ? s : s.slice(0, q); + const query = q === -1 ? '' : s.slice(q); + for (let i = 0; i < 2; i += 1) { + try { + const d = decodeURIComponent(base); + if (d === base) break; + base = d; + } catch (e) { + break; + } + } + return base + query; + } + + function normalizeGameAssetUrlForWebPlay(t) { + let s = String(t || ''); + s = s.replace(/^\/Game\/public\/img\//i, '/Game/img/'); + s = s.replace(/^Game\/public\/img\//i, 'Game/img/'); + return s; + } + + /** แปลง URL จาก Admin/game-timing ให้โหลดได้ (nginx เสิร์ฟจาก /Game/...) */ + function normalizeGauntletAssetUrlForPlay(u) { + if (typeof u !== 'string') return ''; + let raw = normalizeGameAssetUrlForWebPlay(decodeAssetUrlPercentRuns(u.trim())); + const bare0 = raw.split('?')[0].replace(/\/+$/, ''); + if (/^\/Game\/img\/MegaVirus\/Artboard$/i.test(bare0) || /^Game\/img\/MegaVirus\/Artboard$/i.test(bare0)) { + raw = '/Game/img/MegaVirus/Artboard%209.png'; + } + if (!raw) return ''; + if (/^https?:\/\//i.test(raw)) { + const z = raw.replace(/\/+$/, ''); + return z ? encodeSpacesInUrlPath(z) : ''; + } + const qIdx = raw.indexOf('?'); + const base = (qIdx >= 0 ? raw.slice(0, qIdx) : raw).trim().replace(/\/+$/, ''); + const qs = qIdx >= 0 ? raw.slice(qIdx) : ''; + if (!base) return ''; + if (/\.{4,}/.test(base)) return ''; + if (base.startsWith('/')) return encodeSpacesInUrlPath(base + qs); + if (/^Game\//i.test(base)) return encodeSpacesInUrlPath('/' + base.replace(/^\/+/, '') + qs); + return encodeSpacesInUrlPath(base + qs); + } + + function ensureGauntletAssetImage(url) { + const u = normalizeGauntletAssetUrlForPlay(typeof url === 'string' ? url : ''); + if (!u) return null; + let rec = gauntletAssetImageCache.get(u); + if (rec) return rec; + rec = { img: new Image(), ready: false }; + if (/^https?:\/\//i.test(u)) { + try { rec.img.crossOrigin = 'anonymous'; } catch (e) { /* ignore */ } + } + rec.img.onload = function () { rec.ready = true; }; + rec.img.onerror = function () { rec.ready = false; }; + rec.img.src = u; + /** รูปจากแคชบางครั้ง complete ก่อน onload — ต้องเซ็ต ready ทันทีไม่งั้นวาดลูกโป่งไม่ขึ้น */ + if (rec.img.complete && rec.img.naturalWidth > 0) rec.ready = true; + else if (rec.img.complete && rec.img.naturalWidth <= 0) rec.ready = false; + gauntletAssetImageCache.set(u, rec); + return rec; + } + + /** ลูกโป่ง Mega Virus: ถ้ามี URL ต่อที่นั่งแต่โหลดไม่สำเร็จ (404) ให้ใช้ fallback แทน · Per-seat URL wins only when image loads */ + function resolveBalloonBossBalloonSpriteRec(perSeatRaw, fallbackRaw) { + const per = normalizeGauntletAssetUrlForPlay(String(perSeatRaw || '').trim()); + const fb = normalizeGauntletAssetUrlForPlay(String(fallbackRaw || '').trim()); + if (per) { + const rPer = ensureGauntletAssetImage(per); + const img = rPer && rPer.img; + if (img && img.complete && img.naturalWidth > 0) return rPer; + if (img && img.complete && img.naturalWidth <= 0 && fb) return ensureGauntletAssetImage(fb); + return rPer; + } + if (fb) return ensureGauntletAssetImage(fb); + return null; + } + + function pickGauntletLaneImageRec(obsId) { + if (!gauntletLaneImageUrls.length) return null; + const n = gauntletLaneImageUrls.length; + const idx = Math.abs(Number(obsId) | 0) % n; + const u = gauntletLaneImageUrls[idx]; + return u ? ensureGauntletAssetImage(u) : null; + } + + function clampClientLaserColor(s, def) { + const t = String(s || '').trim().slice(0, 100); + if (!t || /[<>"'`]/.test(t)) return def; + return t; + } + + let playQuizPhaseLocal = null; + let playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + let playQuizText = ''; + let playQuizPhaseEndsAt = 0; + let playQuizTimerInterval = null; + /** เกมถูก/ผิด + ภารกิจ mng8a80o: เลขข้อจากเซิร์ฟเวอร์ (1-based) / จำนวนข้อในรอบ — ใช้บรรทัดเล็กเหนือคำถามบนแผนที่ */ + let playQuizQuestionIndex = 0; + let playQuizQuestionTotal = 0; + /** Preview-only: real question pool + phased timer (matches server quiz-settings / map quizQuestions). */ + let previewQuizPool = []; + /** ดัชนีข้อในรอบพรีวิว (0..len-1) — หลังสับแล้วตัดตาม quizRoundQuestionCount เหมือนเซิร์ฟเวอร์ */ + let previewQuizQIndex = 0; + let previewQuizTiming = { readMs: 10000, answerMs: 5000, betweenMs: 3500 }; + let previewQuizStep = 'read'; + let previewQuizCurrent = null; + let playLiveQuizScores = {}; + /** เกมถูก/ผิด (multiplayer): ใครเคยตอบผิดในเซสชันนี้ — ใช้โชว์แสตมป์ในสรุปภารกิจ mng8a80o */ + let playQuizEverWrong = {}; + /** เกมถูก/ผิด — ตรงกับ server QUIZ_TF_POINTS_PER_CORRECT */ + const QUIZ_TF_POINTS_PER_CORRECT = 10; + const QUIZ_TF_SCORE_PLUS_URL = BASE + '/img/QUESTION/score+.png'; + const QUIZ_TF_SCORE_POPUP_MS = 1700; + /** ขนาดป๊อปอัป score+ เทียบกับ tile — เดิม ~1.35×tile; +100% = 2× → 2.7×tile */ + const QUIZ_TF_SCORE_POPUP_TILE_MULT = 2.7; + const QUIZ_TF_SCORE_POPUP_MIN_BASE_PX = 112; + let quizTfScorePopups = []; + let quizTfScorePlusImg = null; + /** คะแนนต่อคำตอบที่ถูกเมื่อส่งป้ายที่ฮับ/โซนส่ง — quiz_carry เท่านั้น */ + const QUIZ_CARRY_POINTS_PER_CORRECT = 10; + /** ครบเซสชัน (แมปสรุปภารกิจ): โชว์ result-complete / gameover กี่ ms ก่อนแผงสรุปผล */ + const QUIZ_CARRY_SESSION_END_SPLASH_MS = 3000; + /** quiz_carry: หยิบตัวเลือกมาวางโซนกลาง — เล่นพร้อมกัน (ไม่สลับตา) */ + let quizCarryPool = []; + let quizCarryCurrent = null; + /** editor embed + preview: นับ 3–2–1 ก่อนโชว์คำถามและให้เดิน/หยิบได้ */ + let quizCarryEmbedPendingQuestion = null; + let quizCarryEmbedCountdownStartAt = 0; + let quizCarryEmbedCountdownEndAt = 0; + /** editor embed: นับ 3–2–1 รอบสองหลังโชว์คำถาม ก่อนเปิดป้ายตัวเลือกบนแมป */ + let quizCarryEmbedPreOptionCountdownStartAt = 0; + let quizCarryEmbedPreOptionCountdownEndAt = 0; + /** quiz_carry: epoch ให้หยิบตัวเลือกได้ · epoch ปิดรอบตอบ (ตั้งที่ Admin แท็บหยิบมาวาง) */ + let quizCarryOptionRevealAt = 0; + let quizCarryAnswerCloseAt = 0; + /** หมดเวลาตอบรอบนี้แล้ว — โชว์ TIME'S UP ~3s แล้วเรียก quizCarryAfterRoundResolved */ + let quizCarryAnswerTimeupAwaitNext = false; + let quizCarryAnswerRoundTimeupAutoT = null; + let quizCarryAnswerRoundTimeupAdvanceAt = 0; + let quizCarryCarryTimingMs = { carryReadMs: 3000, carryAnswerMs: 5000 }; + /** พรีวิว (รวม editor embed): จำนวนข้อที่เล่นก่อนโอเวอร์เลย์จบ — 0 = ไม่จบอัตโนมัติ */ + let quizCarrySessionLength = 0; + let quizCarryRoundsCompleted = 0; + let quizCarrySessionEnded = false; + /** หลัง timeup บนโต๊ะ (embed): รอ 5s แล้วโชว์ result-complete / result-gameover */ + let quizCarryResultEndTimer = null; + /** หลังโชว์ result-end (embed preview): รอ 5s แล้วกลับ room-lobby — ใช้ร่วม quiz_carry + crown */ + let embedPreviewLobbyReturnTimer = null; + /** ครบเซสชัน (mission summary แมป): รอ splash แล้วเปิด #quiz-carry-mission-overlay */ + let quizCarrySessionCompleteResultToSummaryT = null; + /** embed พรีวิว: รอ Ready / START ของโฮสต์ก่อนนับ 3–2–1 */ + let quizCarryPregameActive = false; + let playHostId = null; + /** ซิงก์จากเซิร์ฟเวอร์: socketId → กด Ready แล้ว */ + let quizCarryLobbyReadyMap = {}; + /** จาก /api/quiz-settings carryMapPanelTheme — ใช้กับ #quiz-map-question-panel เฉพาะ quiz_carry */ + let quizCarryMapPanelTheme = null; + /** จาก /api/quiz-settings quizMapPanelTheme — แผงคำถามบนแผนที่ เกมถูก/ผิด (ไม่ใช่ quiz_carry) */ + let quizMapPanelTheme = null; + /** จาก /api/quiz-settings carryEmbedCountdownTheme — สี/ขนาด overlay นับ 3-2-1 (embed) */ + let quizCarryEmbedCountdownTheme = null; + /** จาก /api/quiz-settings carryChoicePlaqueThemes — ป้าย canvas ต่อช่องตัวเลือก 0..15 (null = ใช้ค่าเริ่มต้นทุกช่อง) */ + let quizCarryChoicePlaqueThemes = null; + /** จาก carryChoicePlaqueMapScale — ขยายป้ายบนแมป + ฟอนต์ (0.85–2.5) */ + let quizCarryPlaqueMapScale = 1.25; + const quizCarryChoiceImageCache = new Map(); + /** สแนปจาก join-space (Node) — ใช้เมื่อ fetch HTTP quiz-settings ไม่ได้หรือไม่มีธีม */ + let quizCarryJoinSettingsSnap = null; + + /** quiz_carry — โซนตัวเลือกบนแมป / ธีมป้ายต่อช่องสูงสุด 16 */ + const QUIZ_CARRY_MAX_OPTION_SLOTS = 16; + + /** jump_survive — กล้องตามตัวในกรอบ + แพลตฟอร์มเลื่อนลง (scroll เพิ่ม world Y ของแพลตฟอร์ม) ไม่ลากทั้งฉาก */ + let jumpSurviveCamCenterX = 0; + let jumpSurviveCamCenterY = 0; + let jumpSurviveEliminated = false; + /** จับเวลาโหมดกระโดดให้รอด (วินาทีบน HUD) */ + let jumpSurviveSessionStartMs = 0; + let jumpSurvivePlatformScrollPx = 0; + let jumpSurviveVy = 0; + let jumpSurviveLastTickMs = 0; + let jumpSurviveJumpQueued = false; + let jumpSurviveOnGround = false; + let jumpSurviveLastEmitT = 0; + /** mnptfts2: howto → countdown → live → ended (คะแนน 100 ผู้รอดสุดท้าย) */ + let jumpSurviveMissionPhase = null; + let jumpSurviveGameEnded = false; + let jumperMissionCountdownTimer = null; + + /** space_shooter — ยิงหิน (จุดเกิดจากกริด P1–P6) */ + let spaceShooterBullets = []; + let spaceShooterAsteroids = []; + /** { x, y, r, fi, acc } — fi = index เฟรมแตก (0 = urls[1]) */ + let spaceShooterAsteroidExplosions = []; + let spaceShooterPopups = []; + let spaceShooterLastTickMs = 0; + let spaceShooterSpawnAccMs = 0; + let spaceShooterFireCd = 0; + let spaceShooterSessionStartMs = 0; + let spaceShooterLastMoveEmit = 0; + let spaceShooterGameEnded = false; + let spaceShooterMissionPhase = null; + let spaceShooterMissionCountdownTimer = null; + + /** mng8a80o: howto → countdown → live → ended (สรุปแบบ crown mission) */ + let quizQuestionMissionPhase = null; + let quizQuestionMissionCountdownTimer = null; + let quizQuestionMissionDeferredPhase = null; + /** mng8a80o live: virtual joystick (+x right, +y down) normalized ~[-1,1] */ + let quizQuestionMissionJoyVecX = 0; + let quizQuestionMissionJoyVecY = 0; + let quizQuestionMissionJoyPointerId = null; + /** Stack Tower (mnn93hpi) — flow เดียวกับ crown / quiz mission */ + let stackTowerMissionPhase = null; + let stackTowerMissionCountdownTimer = null; + let stackTowerMissionDeferredPhase = null; + let stackTowerSessionStartAt = 0; + let stackTowerMissionEndedOnce = false; + /** progress ข้ามเกณฑ์ Tower: เริ่มแอนิเมตซูม+เลื่อนแมป — null ยังไม่ข้าม, ตัวเลข = performance.now() ตอนเริ่ม */ + let stackTowerPost50AnimStartMs = null; + /** แฟลชรูปผล Tower (timeup / gameover / complete) ก่อน GCM 5 วิ */ + let stackTowerResultFlashTimer = null; + const STACK_TOWER_RESULT_FLASH_MS = 5000; + + /** รูปอุกาบาตขณะตก: 0 = เต็ม HP (Meteo-1); 1 = โดนยิงแล้วแต่ยังมี HP (Meteo-2 เมื่อมี URL ≥3) */ + function spaceShooterAsteroidLiveSpriteIndexPlay(a) { + const urls = playSpaceShooterAsteroidSpriteUrls; + if (!urls || !urls.length) return 0; + const maxHp = Math.max(1, Math.floor(Number(a && a.maxHp)) || SPACE_SHOOTER_ASTEROID_MAX_HP); + const hp = Math.max(0, Math.floor(Number(a && a.hp)) || maxHp); + if (hp >= maxHp) return 0; + if (urls.length >= 3) return 1; + return 0; + } + + /** เริ่มแอนิเมชันแตกที่เฟรมถัดจาก «โดนแล้ว» เพื่อไม่ซ้ำ Meteo-2 — แสดง Meteo-3 แล้วจบตาม maxFi */ + function spaceShooterAsteroidExplosionStartFiPlay() { + const urls = playSpaceShooterAsteroidSpriteUrls; + if (!urls || urls.length < 2) return 0; + return urls.length >= 3 ? 1 : 0; + } + + function spaceShooterSpawnAsteroidExplosion(worldX, worldY, r) { + const urls = playSpaceShooterAsteroidSpriteUrls; + if (!urls || urls.length < 2) return; + spaceShooterAsteroidExplosions.push({ + x: worldX, + y: worldY, + r: Math.max(6, Number(r) || 14), + fi: spaceShooterAsteroidExplosionStartFiPlay(), + acc: 0, + }); + } + + function spaceShooterTickAsteroidExplosions(dt) { + const urls = playSpaceShooterAsteroidSpriteUrls; + if (!urls || urls.length < 2) { + spaceShooterAsteroidExplosions.length = 0; + return; + } + const fms = Math.max(30, Math.min(500, Number(playSpaceShooterAsteroidExplodeFrameMs) || 70)); + const maxFi = urls.length - 2; + for (let i = spaceShooterAsteroidExplosions.length - 1; i >= 0; i--) { + const ex = spaceShooterAsteroidExplosions[i]; + ex.acc += dt * 1000; + while (ex.acc >= fms) { + ex.acc -= fms; + ex.fi++; + if (ex.fi > maxFi) { + spaceShooterAsteroidExplosions.splice(i, 1); + break; + } + } + } + } + + /** balloon_boss — ลูกโป้งยิงบอส (Mega Virus) */ + let balloonBossPendingShots = []; + let balloonBossPlayerBullets = []; + let balloonBossBossBullets = []; + let balloonBossSessionStartMs = 0; + let balloonBossLastTickMs = 0; + let balloonBossLastMoveEmit = 0; + let balloonBossPlayerFireCd = 0; + let balloonBossGameEnded = false; + let balloonBossHitFx = []; + /** Mega Virus — ป๊อปดาเมจต่อบอส (ข้อความ +2) เมื่อกระสุนโดนบอส */ + let balloonBossScorePopups = []; + let balloonBossBossFireAcc = 0; + /** quiz_battle — โดม MCQ กด E (ข้อจาก battleQuizMcq) */ + let quizBattleMcqPool = []; + let quizBattleAnsweredComps = new Set(); + let quizBattleModalCompId = null; + const SPACE_SHOOTER_SHIP_COLORS = ['#50fa7b', '#ff79c6', '#ff5555', '#f1fa8c', '#bd93f9', '#8be9fd']; + + document.getElementById('room-id').textContent = spaceId; + + function hidePlayQuizHud() { + const sb = document.getElementById('play-quiz-scoreboard'); + if (sb) sb.classList.add('is-hidden'); + const fb = document.getElementById('play-quiz-feedback'); + if (fb) { fb.classList.add('is-hidden'); fb.textContent = ''; } + } + + function renderPlayQuizScoreboard(scores) { + const wrap = document.getElementById('play-quiz-scoreboard'); + const ul = document.getElementById('play-quiz-scoreboard-list'); + if (!wrap || !ul || !mapData || (!isQuiz() && !isQuizCarry() && !isQuizBattle())) return; + if (!scores || typeof scores !== 'object') return; + wrap.classList.remove('is-hidden'); + ul.textContent = ''; + const merged = { ...scores }; + others.forEach((_, id) => { if (merged[id] == null) merged[id] = 0; }); + if (myId != null && merged[myId] == null) merged[myId] = 0; + const rows = []; + if (myId != null) { + rows.push({ id: myId, nick: me.nickname || 'คุณ', sc: merged[myId] != null ? merged[myId] : 0 }); + } + others.forEach((o, id) => { + rows.push({ id, nick: (o && o.nickname) ? String(o.nickname) : id, sc: merged[id] != null ? merged[id] : 0 }); + }); + rows.sort((a, b) => b.sc - a.sc || a.nick.localeCompare(b.nick, 'th')); + rows.forEach((row) => { + const li = document.createElement('li'); + if (row.id === myId) li.className = 'play-quiz-scoreboard-me'; + const spN = document.createElement('span'); + spN.className = 'play-quiz-scoreboard-name'; + spN.textContent = row.nick; + const spV = document.createElement('span'); + spV.className = 'play-quiz-scoreboard-val'; + spV.textContent = String(row.sc); + li.appendChild(spN); + li.appendChild(spV); + ul.appendChild(li); + }); + if (isQuizCarry() && useCyberPlayHud()) { + wrap.classList.add('is-hidden'); + } + /* แมป mng8a80o: คะแนนอยู่ที่ cyber SCORE ซ้ายแล้ว — ซ่อนกล่อง "คะแนน" มุมขวาไม่ให้ซ้ำกับ mock */ + if (isQuizQuestionMissionUiMapPlay()) { + wrap.classList.add('is-hidden'); + } + } + + function initPlayLiveQuizScoresZeros() { + if ((!isQuiz() && !isQuizCarry() && !isQuizBattle()) || myId == null) return; + playLiveQuizScores = {}; + playLiveQuizScores[myId] = 0; + others.forEach((_, id) => { playLiveQuizScores[id] = 0; }); + if (isQuiz()) playQuizEverWrong = {}; + renderPlayQuizScoreboard(playLiveQuizScores); + } + + function showPlayQuizFeedback(r) { + const el = document.getElementById('play-quiz-feedback'); + if (!el || !r || !r.results || myId == null) return; + let mine = null; + let botRight = 0, botWrong = 0; + for (let i = 0; i < r.results.length; i++) { + const row = r.results[i]; + if (row.id === myId) mine = row; + else if (isPreviewBotId(row.id)) { + if (row.right) botRight++; + else botWrong++; + } + } + if (!mine) return; + el.classList.remove('is-hidden'); + const botExtra = playBotsEnabled() && (botRight + botWrong > 0) + ? ' · บอท ถูก ' + botRight + ' / ผิด ' + botWrong + ' (Bots: ' + botRight + ' right / ' + botWrong + ' wrong)' + : ''; + if (mine.right) { + el.className = 'play-quiz-feedback play-quiz-feedback-ok'; + el.textContent = 'คุณตอบถูก · +' + QUIZ_TF_POINTS_PER_CORRECT + ' · คะแนนรวม ' + (typeof mine.score === 'number' ? mine.score : 0) + ' แต้ม' + botExtra; + } else { + el.className = 'play-quiz-feedback play-quiz-feedback-bad'; + el.textContent = (mine.choice == null + ? 'คุณไม่ได้ยืนในโซนตอบ — นับเป็นผิด' + : 'คุณตอบผิด — กลับจุดเกิด และเข้าโซนตอบไม่ได้อีก') + botExtra; + } + if (typeof window.__playQuizFeedbackT === 'number') clearTimeout(window.__playQuizFeedbackT); + window.__playQuizFeedbackT = setTimeout(() => { el.classList.add('is-hidden'); }, 4200); + } + + function ensureQuizTfScorePlusImgPlay() { + if (quizTfScorePlusImg && quizTfScorePlusImg.src) return; + quizTfScorePlusImg = new Image(); + quizTfScorePlusImg.src = QUIZ_TF_SCORE_PLUS_URL; + quizTfScorePlusImg.onerror = function () { + this.onerror = null; + }; + } + + /** โชว์ score+.png กลางโซนจริง/เท็จที่เป็นคำตอบข้อนี้ (world px) */ + function spawnQuizTrueFalseScorePopupPlay(correctTrue) { + if (!isQuiz() || !mapData) return; + const grid = correctTrue ? mapData.quizTrueArea : mapData.quizFalseArea; + const b = getTileBoundsForOnesGrid(grid); + if (!b) return; + const ts = tileSize || 32; + const wx = ((b.minX + b.maxX + 1) / 2) * ts; + const wy = ((b.minY + b.maxY + 1) / 2) * ts; + const now = Date.now(); + ensureQuizTfScorePlusImgPlay(); + quizTfScorePopups.push({ wx, wy, startMs: now, untilMs: now + QUIZ_TF_SCORE_POPUP_MS }); + } + + function drawQuizTfScorePopupsLayer(ctx, worldToScreen, zDraw, timeMs) { + if (!quizTfScorePopups.length) return; + const img = quizTfScorePlusImg; + if (!img || !img.complete || !img.naturalWidth) return; + const now = typeof timeMs === 'number' ? timeMs : Date.now(); + quizTfScorePopups = quizTfScorePopups.filter((p) => p.untilMs > now); + const baseW = Math.max(QUIZ_TF_SCORE_POPUP_MIN_BASE_PX, (tileSize || 32) * zDraw * QUIZ_TF_SCORE_POPUP_TILE_MULT); + for (let i = 0; i < quizTfScorePopups.length; i++) { + const p = quizTfScorePopups[i]; + const span = Math.max(1, p.untilMs - p.startMs); + const t = Math.max(0, Math.min(1, (now - p.startMs) / span)); + const [sx, sy] = worldToScreen(p.wx, p.wy); + const rise = t * 56 * zDraw; + const fade = t < 0.72 ? 1 : Math.max(0, 1 - (t - 0.72) / 0.28); + const pulse = 1 + Math.sin(t * Math.PI) * 0.1; + const scale = (baseW / img.naturalWidth) * pulse; + const w = img.naturalWidth * scale; + const h = img.naturalHeight * scale; + ctx.save(); + ctx.globalAlpha = fade; + ctx.drawImage(img, sx - w / 2, sy - h / 2 - rise, w, h); + ctx.restore(); + } + } + + function getTileBoundsForOnesGrid(grid) { + if (!grid || !grid.length) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (let yy = 0; yy < grid.length; yy++) { + const row = grid[yy]; + if (!row) continue; + for (let xx = 0; xx < row.length; xx++) { + if (Number(row[xx]) === 1) { + if (xx < minX) minX = xx; + if (yy < minY) minY = yy; + if (xx > maxX) maxX = xx; + if (yy > maxY) maxY = yy; + } + } + } + if (minX === Infinity) return null; + return { minX, minY, maxX, maxY }; + } + + function getQuizQuestionAreaTileBounds(md) { + if (!md || md.gameType !== 'quiz') return null; + return getTileBoundsForOnesGrid(md.quizQuestionArea); + } + + /** quiz_carry: โซนทองพิเศษสำหรับข้อความคำถาม (ถ้าไม่วาด ใช้โซนกลางม่วงแทนใน syncPlayQuizMapPanel) */ + function getQuizCarryQuestionAreaTileBounds(md) { + if (!md || md.gameType !== 'quiz_carry') return null; + return getTileBoundsForOnesGrid(md.quizQuestionArea); + } + + function clearQuizMapPanelThemeInline(panel, textEl) { + if (!panel || !textEl) return; + panel.classList.remove('quiz-map-question-panel--has-kicker'); + panel.classList.remove('quiz-map-question-panel--question-mission-live'); + const qKick = document.getElementById('quiz-map-question-kicker'); + if (qKick) { + qKick.textContent = ''; + qKick.classList.add('is-hidden'); + qKick.setAttribute('aria-hidden', 'true'); + } + panel.style.removeProperty('background-image'); + panel.style.removeProperty('background-size'); + panel.style.removeProperty('background-position'); + delete panel.dataset.qmPlaqueTry; + panel.style.removeProperty('--qmap-bg'); + panel.style.removeProperty('--qmap-border'); + panel.style.removeProperty('--qmap-border-w'); + panel.style.removeProperty('--qmap-shadow'); + panel.style.removeProperty('background'); + panel.style.removeProperty('border'); + panel.style.removeProperty('border-color'); + panel.style.removeProperty('border-width'); + panel.style.removeProperty('border-style'); + panel.style.removeProperty('box-shadow'); + textEl.style.removeProperty('--qmap-text'); + textEl.style.removeProperty('--qmap-text-shadow'); + textEl.style.removeProperty('color'); + textEl.style.removeProperty('text-shadow'); + textEl.style.removeProperty('font-size'); + textEl.style.removeProperty('line-height'); + textEl.style.removeProperty('width'); + textEl.style.removeProperty('box-sizing'); + textEl.style.removeProperty('max-height'); + textEl.style.removeProperty('overflow'); + } + + function clearQuizMapQuestionCarryFeedback() { + if (typeof window.__quizMapCarryFeedbackT === 'number') { + clearTimeout(window.__quizMapCarryFeedbackT); + window.__quizMapCarryFeedbackT = null; + } + const icon = document.getElementById('quiz-map-q-feedback-icon'); + const scoreWrap = document.getElementById('quiz-map-q-feedback-score-wrap'); + const scoreEl = document.getElementById('quiz-map-q-feedback-score'); + if (icon) { + icon.classList.add('is-hidden'); + icon.removeAttribute('src'); + } + if (scoreEl) scoreEl.classList.remove('quiz-map-q-feedback-score--pop'); + if (scoreWrap) { + scoreWrap.classList.add('is-hidden'); + scoreWrap.setAttribute('aria-hidden', 'true'); + } + } + + /** ไอคอนถูก/ผิดที่ขอบบนแผง + score+10 ตรงกลางใต้ข้อความ (เฉพาะถูก) — ตาม mock quiz_carry */ + function showQuizMapQuestionCarryFeedback(isCorrect) { + if (!isQuizCarry()) return; + const panel = document.getElementById('quiz-map-question-panel'); + const icon = document.getElementById('quiz-map-q-feedback-icon'); + const scoreWrap = document.getElementById('quiz-map-q-feedback-score-wrap'); + const scoreEl = document.getElementById('quiz-map-q-feedback-score'); + if (!panel || !icon || panel.classList.contains('is-hidden')) return; + clearQuizMapQuestionCarryFeedback(); + icon.src = isCorrect ? QUIZ_CARRY_MAP_FEEDBACK_IMG.correct : QUIZ_CARRY_MAP_FEEDBACK_IMG.incorrect; + icon.classList.remove('is-hidden'); + if (isCorrect && scoreWrap && scoreEl) { + scoreEl.src = QUIZ_CARRY_MAP_FEEDBACK_IMG.scorePlus; + scoreWrap.classList.remove('is-hidden'); + scoreWrap.setAttribute('aria-hidden', 'false'); + scoreEl.classList.remove('quiz-map-q-feedback-score--pop'); + void scoreEl.offsetWidth; + scoreEl.classList.add('quiz-map-q-feedback-score--pop'); + } else if (scoreWrap) { + scoreWrap.classList.add('is-hidden'); + scoreWrap.setAttribute('aria-hidden', 'true'); + } + window.__quizMapCarryFeedbackT = setTimeout(() => { + window.__quizMapCarryFeedbackT = null; + clearQuizMapQuestionCarryFeedback(); + }, QUIZ_MAP_CARRY_FEEDBACK_MS); + } + + /** ค่าเริ่มต้นแผงคำถามบนแผนที่ (ตรงกับ server defaultCarryMapPanelTheme) — ใช้เมื่อยังไม่โหลดจาก API */ + function defaultCarryMapPanelThemePlay() { + return { + panelBg: 'rgba(12, 14, 28, 0.88)', + panelBorder: 'rgba(255, 214, 102, 0.7)', + borderWidthPx: 2, + textColor: '#f1f5f9', + questionFontMinPx: 10, + questionFontMaxPx: 24, + }; + } + + /** ตรงกับ drawQuizCarryChoiceLabels — carryChoicePlaqueMapScale */ + function quizCarryPlaqueMapScaleClampedPlay() { + const sc = Number(quizCarryPlaqueMapScale); + if (!Number.isFinite(sc)) return 1.25; + return Math.max(0.85, Math.min(2.5, sc)); + } + + /** + * ขนาดตัวอักษรแผงคำถาม DOM ให้เท่าป้ายคำตอบบนพื้น: Math.max(10, Math.min(24, tileSize*zoom*0.24*ps)) + * แล้ว clamp ด้วย questionFontMinPx / questionFontMaxPx จากธีม (ถ้าต้องการใหญ่กว่าป้ายได้โดยยกเพดาน max) + */ + function quizMapQuestionFontPxLikeCarryPlaques(th, zoomVal) { + const ts = tileSize * zoomVal; + const ps = quizCarryPlaqueMapScaleClampedPlay(); + const base = Math.max(10, Math.min(24, ts * 0.24 * ps)); + const fb = defaultCarryMapPanelThemePlay(); + let mn = Number(th && th.questionFontMinPx); + let mx = Number(th && th.questionFontMaxPx); + if (!Number.isFinite(mn)) mn = fb.questionFontMinPx; + if (!Number.isFinite(mx)) mx = fb.questionFontMaxPx; + mn = Math.round(Math.max(8, Math.min(40, mn))); + mx = Math.round(Math.max(10, Math.min(72, mx))); + if (mx < mn) { + const s = mn; + mn = mx; + mx = s; + } + return Math.max(mn, Math.min(mx, base)); + } + + /** + * ลด px ทีละ 1 จนเนื้อหาไม่ล้นกล่อง (ไม่ใช้ scrollbar) + * zoom out กล่องเตี้ยมาก: อนุญาตต่ำกว่า questionFontMinPx ของธีมได้ถึง hardMin (≥6) ไม่ให้บรรทัดถูกตัด + * เกมถูก/ผิด: แผงมีคลาส quiz-map-question-panel--true-false จัดข้อความกึ่งกลางแนวตั้งในโซน + */ + function fitQuizMapQuestionFontToPanel(panel, textEl, startPx, minPx) { + const cs = window.getComputedStyle(panel); + const padT = parseFloat(cs.paddingTop) || 0; + const padB = parseFloat(cs.paddingBottom) || 0; + const padL = parseFloat(cs.paddingLeft) || 0; + const padR = parseFloat(cs.paddingRight) || 0; + let kickerReserve = 0; + if (panel.classList.contains('quiz-map-question-panel--question-mission-live') + && panel.classList.contains('quiz-map-question-panel--has-kicker')) { + kickerReserve = 30; + } + const availH = Math.max(12, panel.clientHeight - padT - padB - kickerReserve); + const availW = Math.max(24, panel.clientWidth - padL - padR); + const themeMin = Math.max(8, Math.floor(Number(minPx) || 8)); + const hardMin = Math.min(themeMin, Math.max(6, Math.floor(availH / 7))); + let px = Math.round(Math.min(80, Math.max(hardMin, Number(startPx) || hardMin))); + textEl.style.setProperty('line-height', '1.28', 'important'); + textEl.style.setProperty('width', '100%', 'important'); + textEl.style.setProperty('box-sizing', 'border-box', 'important'); + textEl.style.removeProperty('max-height'); + textEl.style.setProperty('overflow', 'hidden', 'important'); + for (let i = 0; i < 96 && px > hardMin; i++) { + textEl.style.setProperty('font-size', String(px) + 'px', 'important'); + const sh = textEl.scrollHeight; + const sw = textEl.scrollWidth; + if (sh <= availH + 2 && sw <= availW + 2) break; + px -= 1; + } + textEl.style.setProperty('font-size', String(px) + 'px', 'important'); + textEl.style.setProperty('max-height', availH + 'px', 'important'); + } + + /** ธีมจาก quiz-settings / join API — ถ้า JSON ไม่มีฟอนต์ให้ใช้ค่าเริ่มต้น */ + function parseCarryMapPanelThemeObject(raw) { + let t = raw; + if (typeof t === 'string') { + try { + t = JSON.parse(t); + } catch (e) { + t = null; + } + } + if (!t || typeof t !== 'object') return null; + const bw = Number(t.borderWidthPx); + const borderWidthPx = Number.isFinite(bw) ? Math.max(0, Math.min(12, Math.round(bw))) : 2; + const fb = defaultCarryMapPanelThemePlay(); + let qMin = Number(t.questionFontMinPx); + let qMax = Number(t.questionFontMaxPx); + if (!Number.isFinite(qMin)) qMin = fb.questionFontMinPx; + if (!Number.isFinite(qMax)) qMax = fb.questionFontMaxPx; + qMin = Math.round(Math.max(10, Math.min(40, qMin))); + qMax = Math.round(Math.max(14, Math.min(56, qMax))); + if (qMax < qMin) { + const s = qMin; + qMin = qMax; + qMax = s; + } + return { + panelBg: String(t.panelBg || '').trim().slice(0, 120), + panelBorder: String(t.panelBorder || '').trim().slice(0, 120), + borderWidthPx, + textColor: String(t.textColor || '').trim().slice(0, 120), + questionFontMinPx: qMin, + questionFontMaxPx: qMax, + }; + } + + /** + * ชั้นทับจาก carryMapPanelTheme ในไฟล์แมป — ฟอนต์ใส่เฉพาะเมื่อ JSON มี key จริง + * (กันธีมแมปมีแค่สีแล้วไปทับขนาดฟอนต์จาก Admin เป็นค่าเริ่ม 16/24) + */ + function parseCarryMapPanelThemeMapOverlay(raw) { + let t = raw; + if (typeof t === 'string') { + try { + t = JSON.parse(t); + } catch (e) { + t = null; + } + } + if (!t || typeof t !== 'object') return null; + const bw = Number(t.borderWidthPx); + const borderWidthPx = Number.isFinite(bw) ? Math.max(0, Math.min(12, Math.round(bw))) : 2; + const out = { + panelBg: String(t.panelBg || '').trim().slice(0, 120), + panelBorder: String(t.panelBorder || '').trim().slice(0, 120), + borderWidthPx, + textColor: String(t.textColor || '').trim().slice(0, 120), + }; + const hasMin = Object.prototype.hasOwnProperty.call(t, 'questionFontMinPx'); + const hasMax = Object.prototype.hasOwnProperty.call(t, 'questionFontMaxPx'); + if (hasMin || hasMax) { + const fb = defaultCarryMapPanelThemePlay(); + let qMin = hasMin ? Number(t.questionFontMinPx) : fb.questionFontMinPx; + let qMax = hasMax ? Number(t.questionFontMaxPx) : fb.questionFontMaxPx; + if (!Number.isFinite(qMin)) qMin = fb.questionFontMinPx; + if (!Number.isFinite(qMax)) qMax = fb.questionFontMaxPx; + qMin = Math.round(Math.max(10, Math.min(40, qMin))); + qMax = Math.round(Math.max(14, Math.min(56, qMax))); + if (qMax < qMin) { + const s = qMin; + qMin = qMax; + qMax = s; + } + out.questionFontMinPx = qMin; + out.questionFontMaxPx = qMax; + } + return out; + } + + function setQuizCarryMapPanelThemeFromApi(s) { + quizCarryMapPanelTheme = null; + if (!s || typeof s !== 'object') return; + const parsed = parseCarryMapPanelThemeObject(s.carryMapPanelTheme); + if (!parsed) return; + quizCarryMapPanelTheme = parsed; + } + + function setQuizMapPanelThemeFromApi(s) { + quizMapPanelTheme = null; + if (!s || typeof s !== 'object') return; + const parsed = parseCarryMapPanelThemeObject(s.quizMapPanelTheme); + if (!parsed) return; + quizMapPanelTheme = parsed; + } + + function defaultCarryChoicePlaqueThemePlay() { + return { + borderMode: 'neon', + fixedBorder: 'rgba(122, 200, 255, 0.9)', + fillBg: 'rgba(12, 10, 20, 0.88)', + textColor: 'rgba(248, 249, 255, 1)', + borderWidthPx: 2.5, + plaqueImageUrl: '', + }; + } + + function parseCarryChoicePlaqueThemeObject(raw) { + const d = defaultCarryChoicePlaqueThemePlay(); + if (!raw || typeof raw !== 'object') return d; + const safeColor = (x, fb) => { + const t = String(x == null ? '' : x).trim().slice(0, 120); + if (!t || /url\s*\(|expression|@import|\/\*|javascript|<|>|\\0/i.test(t)) return fb; + if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(t)) return t; + if (/^rgba?\(\s*[^)]+\)$/i.test(t)) return t.replace(/\s+/g, ' ').trim(); + return fb; + }; + const mode = String(raw.borderMode || '').toLowerCase() === 'fixed' ? 'fixed' : 'neon'; + let bw = Number(raw.borderWidthPx); + if (!Number.isFinite(bw)) bw = d.borderWidthPx; + bw = Math.round(Math.max(0, Math.min(8, bw)) * 10) / 10; + return { + borderMode: mode, + fixedBorder: safeColor(raw.fixedBorder, d.fixedBorder), + fillBg: safeColor(raw.fillBg, d.fillBg), + textColor: safeColor(raw.textColor, d.textColor), + borderWidthPx: bw, + plaqueImageUrl: sanitizeQuizCarryImageUrlClient(raw.plaqueImageUrl), + }; + } + + function buildCarryChoicePlaqueThemesArrayFromApi(s) { + if (!s || typeof s !== 'object') return null; + if (Array.isArray(s.carryChoicePlaqueThemes) && s.carryChoicePlaqueThemes.length) { + const out = []; + for (let i = 0; i < QUIZ_CARRY_MAX_OPTION_SLOTS; i++) { + out.push(parseCarryChoicePlaqueThemeObject(s.carryChoicePlaqueThemes[i])); + } + return out; + } + if (s.carryChoicePlaqueTheme && typeof s.carryChoicePlaqueTheme === 'object') { + const one = parseCarryChoicePlaqueThemeObject(s.carryChoicePlaqueTheme); + return Array.from({ length: QUIZ_CARRY_MAX_OPTION_SLOTS }, () => ({ ...one })); + } + return null; + } + + function getEffectiveCarryChoicePlaqueThemeForChoice(choiceIndex) { + const i = Math.max(0, Math.min(QUIZ_CARRY_MAX_OPTION_SLOTS - 1, choiceIndex | 0)); + if (quizCarryChoicePlaqueThemes && quizCarryChoicePlaqueThemes[i]) { + return quizCarryChoicePlaqueThemes[i]; + } + return defaultCarryChoicePlaqueThemePlay(); + } + + function setQuizCarryChoicePlaqueThemeFromApi(s) { + quizCarryChoicePlaqueThemes = buildCarryChoicePlaqueThemesArrayFromApi(s); + } + + function sanitizeQuizCarryImageUrlClient(u) { + const s = String(u == null ? '' : u).trim().slice(0, 512); + if (!s || /[\s<>'"`]/.test(s)) return ''; + if (s.startsWith('/')) { + if (!/^\/[\w\-./?#=&%+]+$/i.test(s)) return ''; + return s; + } + const low = s.toLowerCase(); + if (!low.startsWith('https://') && !low.startsWith('http://')) return ''; + try { + const parsed = new URL(s); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return ''; + return s; + } catch (e) { + return ''; + } + } + + /** crossOrigin=anonymous กับ same-origin บางเซิร์ฟ/nginx ทำให้โหลดรูปล้ม — ใช้เฉพาะข้ามโดเมน */ + function quizCarryApplyImageCrossOrigin(img, rawUrl) { + if (!img) return; + try { + img.removeAttribute('crossorigin'); + const u = sanitizeQuizCarryImageUrlClient(rawUrl); + if (!u || u.startsWith('data:')) return; + let imgOrigin = ''; + if (u.startsWith('http://') || u.startsWith('https://')) { + imgOrigin = new URL(u).origin; + } else if (u.startsWith('/')) { + imgOrigin = window.location.origin; + } + if (imgOrigin && imgOrigin !== window.location.origin) { + img.crossOrigin = 'anonymous'; + } + } catch (e) { /* ignore */ } + } + + function preloadQuizCarryChoiceImages(q) { + if (!q) return; + if (Array.isArray(q.choiceImageUrls) && q.choiceImageUrls.length) { + for (let i = 0; i < q.choiceImageUrls.length; i++) { + const raw = sanitizeQuizCarryImageUrlClient(q.choiceImageUrls[i]); + if (!raw) continue; + if (quizCarryChoiceImageCache.has(raw)) continue; + const im = new Image(); + quizCarryApplyImageCrossOrigin(im, raw); + im.onload = () => { try { draw(); } catch (e) { /* ignore */ } }; + quizCarryChoiceImageCache.set(raw, im); + try { + im.src = raw; + } catch (e) { /* ignore */ } + } + } + if (Array.isArray(q.choices)) { + for (let j = 0; j < q.choices.length; j++) { + const raw2 = getQuizCarryPlaqueImageUrlForIndex(q, j); + if (!raw2 || quizCarryChoiceImageCache.has(raw2)) continue; + const im2 = new Image(); + quizCarryApplyImageCrossOrigin(im2, raw2); + im2.onload = () => { try { draw(); } catch (e) { /* ignore */ } }; + quizCarryChoiceImageCache.set(raw2, im2); + try { + im2.src = raw2; + } catch (e) { /* ignore */ } + } + } + } + + function getQuizCarryChoiceImageCached(url) { + const u = sanitizeQuizCarryImageUrlClient(url); + if (!u) return null; + const im = quizCarryChoiceImageCache.get(u); + if (im && im.complete && im.naturalWidth > 0) return im; + return null; + } + + function getQuizCarryChoiceImageUrlForIndex(q, idx) { + if (!q || !Array.isArray(q.choiceImageUrls)) return ''; + const u = q.choiceImageUrls[idx]; + return sanitizeQuizCarryImageUrlClient(u); + } + + /** รูปป้าย: คำถามต่อช่องก่อน แล้วค่อยรูปจากธีมช่อง (carryChoicePlaqueThemes) */ + function getQuizCarryPlaqueImageUrlForIndex(q, idx) { + const perQ = getQuizCarryChoiceImageUrlForIndex(q, idx); + if (perQ) return perQ; + const th = getEffectiveCarryChoicePlaqueThemeForChoice(idx); + const u = th && th.plaqueImageUrl ? sanitizeQuizCarryImageUrlClient(th.plaqueImageUrl) : ''; + return u || ''; + } + + function defaultCarryEmbedCountdownThemePlay() { + return { + overlayBackdrop: 'rgba(6, 8, 14, 0.52)', + innerBg: 'rgba(0, 0, 0, 0.78)', + innerBorder: 'rgba(94, 234, 212, 0.82)', + innerBorderWpx: 1, + innerRadiusPx: 14, + digitColor: '#ffe066', + mapDigitCqmin: 78, + mapDigitCqh: 82, + mapDigitMaxPx: 220, + screenDigitVw: 32, + screenDigitMaxPx: 200, + }; + } + + function parseCarryEmbedCountdownThemeObject(raw) { + const d = defaultCarryEmbedCountdownThemePlay(); + if (!raw || typeof raw !== 'object') return d; + const clampN = (v, lo, hi, def) => { + const n = Number(v); + if (!Number.isFinite(n)) return def; + return Math.max(lo, Math.min(hi, Math.round(n))); + }; + const clipStr = (x, maxLen) => { + const t = String(x == null ? '' : x).trim().slice(0, maxLen); + return t || null; + }; + const safeColor = (x, fb) => { + const t = clipStr(x, 120); + if (!t || /url\s*\(|expression|@import|\/\*|javascript|<|>|\\0/i.test(t)) return fb; + if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(t)) return t; + if (/^rgba?\(\s*[^)]+\)$/i.test(t)) return t.replace(/\s+/g, ' ').trim(); + return fb; + }; + return { + overlayBackdrop: safeColor(raw.overlayBackdrop, d.overlayBackdrop), + innerBg: safeColor(raw.innerBg, d.innerBg), + innerBorder: safeColor(raw.innerBorder, d.innerBorder), + innerBorderWpx: clampN(raw.innerBorderWpx, 0, 12, d.innerBorderWpx), + innerRadiusPx: clampN(raw.innerRadiusPx, 0, 32, d.innerRadiusPx), + digitColor: safeColor(raw.digitColor, d.digitColor), + mapDigitCqmin: clampN(raw.mapDigitCqmin, 35, 100, d.mapDigitCqmin), + mapDigitCqh: clampN(raw.mapDigitCqh, 35, 100, d.mapDigitCqh), + mapDigitMaxPx: clampN(raw.mapDigitMaxPx, 48, 400, d.mapDigitMaxPx), + screenDigitVw: clampN(raw.screenDigitVw, 6, 44, d.screenDigitVw), + screenDigitMaxPx: clampN(raw.screenDigitMaxPx, 48, 220, d.screenDigitMaxPx), + }; + } + + function setQuizCarryEmbedCountdownThemeFromApi(s) { + quizCarryEmbedCountdownTheme = parseCarryEmbedCountdownThemeObject(s && s.carryEmbedCountdownTheme); + applyQuizCarryEmbedCountdownThemeToDom(); + } + + const CARRY_ECD_VAR_KEYS = [ + '--carry-ecd-overlay', + '--carry-ecd-inner-bg', + '--carry-ecd-inner-border', + '--carry-ecd-inner-border-w', + '--carry-ecd-inner-radius', + '--carry-ecd-digit-color', + '--carry-ecd-map-digit-fs', + '--carry-ecd-screen-vw', + '--carry-ecd-screen-max', + ]; + + function clearQuizCarryEmbedCountdownThemeDomVars() { + const root = document.getElementById('quiz-carry-embed-countdown'); + if (!root) return; + CARRY_ECD_VAR_KEYS.forEach((k) => { try { root.style.removeProperty(k); } catch (e) { /* ignore */ } }); + } + + /** ใส่ CSS variables บน #quiz-carry-embed-countdown — อ่านจาก quizCarryEmbedCountdownTheme */ + function applyQuizCarryEmbedCountdownThemeToDom() { + const root = document.getElementById('quiz-carry-embed-countdown'); + const numEl = document.getElementById('quiz-carry-embed-countdown-num'); + if (!root) return; + clearQuizCarryEmbedCountdownThemeDomVars(); + const th = quizCarryEmbedCountdownTheme || defaultCarryEmbedCountdownThemePlay(); + root.style.setProperty('--carry-ecd-overlay', th.overlayBackdrop); + root.style.setProperty('--carry-ecd-inner-bg', th.innerBg); + root.style.setProperty('--carry-ecd-inner-border', th.innerBorder); + root.style.setProperty('--carry-ecd-inner-border-w', `${Math.max(0, Math.min(12, Math.round(Number(th.innerBorderWpx) || 1)))}px`); + root.style.setProperty('--carry-ecd-inner-radius', `${Math.max(0, Math.min(32, Math.round(Number(th.innerRadiusPx) || 12)))}px`); + root.style.setProperty('--carry-ecd-digit-color', th.digitColor); + const cqmin = Math.max(35, Math.min(100, Math.round(Number(th.mapDigitCqmin) || 78))); + const cqh = Math.max(35, Math.min(100, Math.round(Number(th.mapDigitCqh) || 82))); + const mpx = Math.max(48, Math.min(400, Math.round(Number(th.mapDigitMaxPx) || 200))); + root.style.setProperty('--carry-ecd-map-digit-fs', `clamp(14px, min(${cqmin}cqmin, ${cqh}cqh), ${mpx}px)`); + const vw = Math.max(6, Math.min(44, Math.round(Number(th.screenDigitVw) || 28))); + const smx = Math.max(48, Math.min(220, Math.round(Number(th.screenDigitMaxPx) || 132))); + root.style.setProperty('--carry-ecd-screen-vw', `${vw}vw`); + root.style.setProperty('--carry-ecd-screen-max', `${smx}px`); + if (numEl && String(numEl.tagName || '').toUpperCase() !== 'IMG') { + numEl.style.textShadow = '0 0 0.45em currentColor, 0 4px 14px rgba(0,0,0,0.55)'; + } else if (numEl) { + try { numEl.style.removeProperty('text-shadow'); } catch (e) { /* ignore */ } + } + } + + function setCountdown321QuestionAssetGraphic(numEl, n) { + if (!numEl) return; + const d = Math.max(1, Math.min(3, n)); + if (String(numEl.tagName || '').toUpperCase() === 'IMG') { + numEl.src = COUNTDOWN_321_IMG_BASE + d + '.png'; + numEl.alt = String(d); + } else { + numEl.textContent = String(d); + } + } + + /** default ← quiz-settings/API ← แมป (สีจากแมปทับได้; ฟอนต์จากแมปเฉพาะเมื่อแมประบุ key) */ + function getEffectiveCarryMapPanelThemeForApply() { + const d = defaultCarryMapPanelThemePlay(); + const api = quizCarryMapPanelTheme != null && typeof quizCarryMapPanelTheme === 'object' ? quizCarryMapPanelTheme : null; + let merged = api ? { ...d, ...api } : { ...d }; + if (mapData && mapData.carryMapPanelTheme != null) { + const mp = parseCarryMapPanelThemeMapOverlay(mapData.carryMapPanelTheme); + if (mp) merged = { ...merged, ...mp }; + } + return merged; + } + + /** เกมถูก/ผิด: default ← quizMapPanelTheme (API) ← quizMapPanelTheme บนแมป (ถ้ามี) */ + function getEffectiveQuizMapPanelThemeForApply() { + const d = defaultCarryMapPanelThemePlay(); + const api = quizMapPanelTheme != null && typeof quizMapPanelTheme === 'object' ? quizMapPanelTheme : null; + let merged = api ? { ...d, ...api } : { ...d }; + if (mapData && mapData.quizMapPanelTheme != null) { + const mp = parseCarryMapPanelThemeMapOverlay(mapData.quizMapPanelTheme); + if (mp) merged = { ...merged, ...mp }; + } + return merged; + } + + function paintQuizMapPanelTheme(panel, textEl, th) { + if (!panel || !textEl || !th) return; + clearQuizMapPanelThemeInline(panel, textEl); + const bgStr = th.panelBg != null ? String(th.panelBg).trim() : ''; + const brStr = th.panelBorder != null ? String(th.panelBorder).trim() : ''; + const txStr = th.textColor != null ? String(th.textColor).trim() : ''; + const wRaw = Number(th.borderWidthPx); + const w = Number.isFinite(wRaw) ? Math.max(0, Math.min(12, Math.round(wRaw))) : 0; + const bgUse = bgStr || 'transparent'; + const brUse = brStr || 'transparent'; + const txUse = txStr || '#f1f5f9'; + panel.style.setProperty('--qmap-bg', bgUse); + panel.style.setProperty('--qmap-border', brUse); + panel.style.setProperty('--qmap-border-w', String(w) + 'px'); + panel.style.setProperty('--qmap-shadow', 'none'); + textEl.style.setProperty('--qmap-text', txUse); + textEl.style.setProperty('--qmap-text-shadow', 'none'); + /* โหลด play.html แบบแคชเก่า (ไม่มี var) ยังต้องทับได้ — ใช้ !important คู่กับตัวแปร */ + panel.style.setProperty('background', bgUse, 'important'); + panel.style.setProperty('border', String(w) + 'px solid ' + brUse, 'important'); + panel.style.setProperty('box-shadow', 'none', 'important'); + textEl.style.setProperty('color', txUse, 'important'); + textEl.style.setProperty('text-shadow', 'none', 'important'); + } + + function applyQuizCarryMapPanelThemeIfNeeded(panel, textEl) { + if (!panel || !textEl) return; + if (!isQuizCarry()) { + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + paintQuizMapPanelTheme(panel, textEl, getEffectiveCarryMapPanelThemeForApply()); + } + + function applyQuizTrueFalseMapPanelThemeIfNeeded(panel, textEl) { + if (!panel || !textEl) return; + if (!isQuiz() || isQuizCarry()) return; + paintQuizMapPanelTheme(panel, textEl, getEffectiveQuizMapPanelThemeForApply()); + } + + function syncPlayQuizMapPanel() { + const panel = document.getElementById('quiz-map-question-panel'); + const textEl = document.getElementById('quiz-map-question-text'); + if (!panel || !textEl || !mapData) return; + /* เอดิเตอร์ embed: ซ่อนแผงทองเฉพาะ quiz ทั่วไป — แมปภารกิจ mng8a80o ต้องโชว์คำถามในโซนที่วาด (quizQuestionArea) */ + if (previewMode && editorEmbedReturn && !isQuizCarry() && !(isQuiz() && isQuizQuestionMissionUiMapPlay())) { + clearQuizMapQuestionCarryFeedback(); + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + if (isQuizBattle()) { + clearQuizMapQuestionCarryFeedback(); + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + if (!isQuiz() && !isQuizCarry()) { + clearQuizMapQuestionCarryFeedback(); + return; + } + const bounds = isQuiz() + ? getQuizQuestionAreaTileBounds(mapData) + : (getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData)); + const text = (playQuizText || '').trim(); + if (!bounds || !text) { + clearQuizMapQuestionCarryFeedback(); + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, textEl); + return; + } + const qmCenter = isQuizQuestionMissionHudActivePlay() ? getQuizQuestionMissionMapCenterWorldPxPlay() : null; + const carryCam = !qmCenter && isQuizCarry() ? getQuizCarryMapCameraWorldCenterPxPlay() : null; + const camX = qmCenter ? qmCenter.cx : (carryCam ? carryCam.cx : me.x * tileSize); + const camY = qmCenter ? qmCenter.cy : (carryCam ? carryCam.cy : me.y * tileSize); + const zDom = playDomSyncZoom(); + const left = (bounds.minX * tileSize - camX) * zDom + canvas.width / 2; + const top = (bounds.minY * tileSize - camY) * zDom + canvas.height / 2; + const wPx = (bounds.maxX - bounds.minX + 1) * tileSize * zDom; + const hPx = (bounds.maxY - bounds.minY + 1) * tileSize * zDom; + textEl.textContent = text; + panel.style.left = Math.round(left) + 'px'; + panel.style.top = Math.round(top) + 'px'; + panel.style.width = Math.round(Math.max(48, wPx)) + 'px'; + panel.style.height = Math.round(Math.max(40, hPx)) + 'px'; + panel.classList.remove('is-hidden'); + panel.setAttribute('aria-hidden', 'false'); + if (isQuiz() && !isQuizCarry()) { + panel.classList.add('quiz-map-question-panel--true-false'); + } else { + panel.classList.remove('quiz-map-question-panel--true-false'); + } + if (isQuizCarry()) { + applyQuizCarryMapPanelThemeIfNeeded(panel, textEl); + } else if (isQuiz()) { + applyQuizTrueFalseMapPanelThemeIfNeeded(panel, textEl); + } else { + clearQuizMapPanelThemeInline(panel, textEl); + } + const thFont = isQuizCarry() + ? getEffectiveCarryMapPanelThemeForApply() + : getEffectiveQuizMapPanelThemeForApply(); + const startFontPx = quizMapQuestionFontPxLikeCarryPlaques(thFont, zDom); + const mnFont = Math.max(8, Math.round(Number(thFont.questionFontMinPx) || 10)); + /* ไม่โหลด hud-question-plaque.png อัตโนมัติ — บนเซิร์ฟมักไม่มีไฟล์ → 404 ใน Network; ใช้คลาส + CSS แทน (อัปโหลด PNG แล้วค่อยผูกทีหลังได้) */ + if (isQuiz() && isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'live') { + panel.classList.add('quiz-map-question-panel--question-mission-live'); + } else { + panel.classList.remove('quiz-map-question-panel--question-mission-live'); + if (panel.dataset.qmPlaqueTry) { + panel.style.removeProperty('background-image'); + panel.style.removeProperty('background-size'); + panel.style.removeProperty('background-position'); + delete panel.dataset.qmPlaqueTry; + } + } + const kickerEl = document.getElementById('quiz-map-question-kicker'); + const missionLiveQ = isQuiz() && isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'live'; + const inReadOrAnswer = playQuizPhaseLocal === 'read' || playQuizPhaseLocal === 'answer'; + if (kickerEl) { + let showK = false; + let line = ''; + if (missionLiveQ && inReadOrAnswer) { + const qi = Math.max(0, Number(playQuizQuestionIndex) || 0); + const qt = Math.max(0, Number(playQuizQuestionTotal) || 0); + if (qi > 0 && qt > 0) { + line = '- Quiz ' + qi + ' / ' + qt + ' -'; + showK = true; + } else if (qi > 0) { + line = '- Quiz ' + qi + ' -'; + showK = true; + } + } + if (showK && line) { + kickerEl.textContent = line; + kickerEl.classList.remove('is-hidden'); + kickerEl.setAttribute('aria-hidden', 'false'); + panel.classList.add('quiz-map-question-panel--has-kicker'); + } else { + kickerEl.textContent = ''; + kickerEl.classList.add('is-hidden'); + kickerEl.setAttribute('aria-hidden', 'true'); + panel.classList.remove('quiz-map-question-panel--has-kicker'); + } + } + fitQuizMapQuestionFontToPanel(panel, textEl, startFontPx, mnFont); + } + + /** โซนเดียวกับแผงคำถามบนแผนที่ — ยึด timeup ให้ตรงกล่อง #quiz-map-question-panel (ห้ามคำนวณซ้ำแล้วคลาด) */ + function syncQuizCarryTimeupDeskLayerPosition() { + const layer = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!layer || layer.classList.contains('is-hidden')) return; + const anchor = layer.querySelector('.qc-timeup-desk-anchor') + || layer.querySelector('.qc-timeup-desk-inner') + || (() => { + const img = document.getElementById('quiz-carry-timeup-txt-img'); + return img && img.parentElement && img.parentElement !== layer ? img.parentElement : null; + })(); + if (!anchor || !canvas) return; + const clearToFullStack = () => { + anchor.style.left = '0'; + anchor.style.top = '0'; + anchor.style.width = '100%'; + anchor.style.height = '100%'; + }; + const stack = document.getElementById('play-canvas-stack'); + const panel = document.getElementById('quiz-map-question-panel'); + if (stack && panel && !panel.classList.contains('is-hidden')) { + try { + const sr = stack.getBoundingClientRect(); + const pr = panel.getBoundingClientRect(); + const rw = pr.width; + const rh = pr.height; + if (rw >= 8 && rh >= 8 && sr.width > 0 && sr.height > 0) { + anchor.style.left = Math.round(pr.left - sr.left) + 'px'; + anchor.style.top = Math.round(pr.top - sr.top) + 'px'; + anchor.style.width = Math.round(rw) + 'px'; + anchor.style.height = Math.round(rh) + 'px'; + return; + } + } catch (e) { /* fallback ด้านล่าง */ } + } + const pl = panel && panel.style && String(panel.style.left || '').trim(); + const pt = panel && panel.style && String(panel.style.top || '').trim(); + const pw = panel && panel.style && String(panel.style.width || '').trim(); + const ph = panel && panel.style && String(panel.style.height || '').trim(); + if (pl && pt && pw && ph) { + anchor.style.left = pl; + anchor.style.top = pt; + anchor.style.width = pw; + anchor.style.height = ph; + return; + } + if (!mapData || mapData.gameType !== 'quiz_carry') { + clearToFullStack(); + return; + } + const bounds = getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData); + if (!bounds) { + clearToFullStack(); + return; + } + const cc = getQuizCarryMapCameraWorldCenterPxPlay(); + const camX = cc ? cc.cx : me.x * tileSize; + const camY = cc ? cc.cy : me.y * tileSize; + const zDom = playDomSyncZoom(); + const left = (bounds.minX * tileSize - camX) * zDom + canvas.width / 2; + const top = (bounds.minY * tileSize - camY) * zDom + canvas.height / 2; + const wPx = (bounds.maxX - bounds.minX + 1) * tileSize * zDom; + const hPx = (bounds.maxY - bounds.minY + 1) * tileSize * zDom; + anchor.style.left = Math.round(left) + 'px'; + anchor.style.top = Math.round(top) + 'px'; + anchor.style.width = Math.round(Math.max(48, wPx)) + 'px'; + anchor.style.height = Math.round(Math.max(40, hPx)) + 'px'; + } + + function carryEmbedCountdownAnchorResolved() { + if (!mapData || !isQuizCarry()) return 'screen'; + const v = mapData.carryEmbedCountdownAnchor; + if (v === 'grid' || v === 'map') return v; + return 'screen'; + } + + function resetQuizCarryEmbedCountdownLayoutDom() { + const ov = document.getElementById('quiz-carry-embed-countdown'); + const inner = document.getElementById('quiz-carry-embed-countdown-inner'); + if (ov) ov.classList.remove('quiz-carry-embed-countdown--map-anchor'); + if (inner) { + inner.classList.remove('quiz-carry-embed-countdown-inner--map-rect'); + inner.style.position = ''; + inner.style.left = ''; + inner.style.top = ''; + inner.style.width = ''; + inner.style.height = ''; + inner.style.transform = ''; + inner.style.maxWidth = ''; + inner.style.boxSizing = ''; + inner.style.display = ''; + inner.style.margin = ''; + } + } + + /** พรีวิว embed: วางกล่อง 3–2–1 กลางจอ / ชิดโซนทองหรือโซนกลาง / ชิดกริด carryEmbedCountdownArea — ตาม carryEmbedCountdownAnchor */ + function syncQuizCarryEmbedCountdownLayout() { + if (!previewMode || !editorEmbedReturn || !mapData || !isQuizCarry()) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + const ov = document.getElementById('quiz-carry-embed-countdown'); + const inner = document.getElementById('quiz-carry-embed-countdown-inner'); + const canvasEl = document.getElementById('game-canvas'); + if (!ov || !inner || !canvasEl) return; + if (ov.classList.contains('is-hidden')) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + const anchor = carryEmbedCountdownAnchorResolved(); + if (anchor === 'screen') { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + let bounds = null; + if (anchor === 'grid') { + bounds = getTileBoundsForOnesGrid(mapData.carryEmbedCountdownArea); + if (!bounds) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + } else { + bounds = getQuizCarryQuestionAreaTileBounds(mapData) || getQuizCarryHubTileBounds(mapData); + if (!bounds) { + resetQuizCarryEmbedCountdownLayoutDom(); + return; + } + } + const cc = getQuizCarryMapCameraWorldCenterPxPlay(); + const camX = cc ? cc.cx : me.x * tileSize; + const camY = cc ? cc.cy : me.y * tileSize; + const zDom = playDomSyncZoom(); + const l = (bounds.minX * tileSize - camX) * zDom + canvasEl.width / 2; + const t = (bounds.minY * tileSize - camY) * zDom + canvasEl.height / 2; + const wPx = (bounds.maxX - bounds.minX + 1) * tileSize * zDom; + const hPx = (bounds.maxY - bounds.minY + 1) * tileSize * zDom; + const r = canvasEl.getBoundingClientRect(); + const sl = r.left + l; + const st = r.top + t; + ov.classList.add('quiz-carry-embed-countdown--map-anchor'); + inner.classList.add('quiz-carry-embed-countdown-inner--map-rect'); + inner.style.position = 'fixed'; + inner.style.left = Math.round(sl) + 'px'; + inner.style.top = Math.round(st) + 'px'; + inner.style.width = Math.round(Math.max(100, wPx)) + 'px'; + inner.style.height = Math.round(Math.max(72, hPx)) + 'px'; + inner.style.transform = 'none'; + inner.style.boxSizing = 'border-box'; + inner.style.display = 'flex'; + inner.style.flexDirection = 'column'; + inner.style.alignItems = 'center'; + inner.style.justifyContent = 'center'; + inner.style.margin = '0'; + inner.style.maxWidth = 'none'; + } + + /** quiz_carry + embed: คำถามไปที่แผงทองบนแผนที่ / ใน overlay นับถอยหลัง — ไม่ใช้แถบบน */ + function syncQuizCarryEmbedQuestionStrip() { + const wrap = document.getElementById('quiz-carry-embed-q-strip'); + const p = document.getElementById('quiz-carry-embed-q-strip-text'); + if (!wrap || !p) return; + wrap.classList.remove('quiz-carry-embed-q-strip--countdown'); + wrap.classList.add('is-hidden'); + wrap.setAttribute('aria-hidden', 'true'); + } + + function updatePlayQuizTimerDisplay() { + const el = document.getElementById('quiz-game-timer'); + if (!el) return; + if (!playQuizPhaseEndsAt) { + el.textContent = ''; + return; + } + const s = Math.max(0, Math.ceil((playQuizPhaseEndsAt - Date.now()) / 1000)); + el.textContent = s + ' วินาที'; + } + + function clampPreviewMs(n, def, minV, maxV) { + const v = Number(n); + if (Number.isNaN(v)) return def; + return Math.max(minV, Math.min(maxV, Math.floor(v))); + } + + function clampCarrySessionLen(n, def) { + const v = Number(n); + if (Number.isNaN(v)) return def; + return Math.max(0, Math.min(500, Math.floor(v))); + } + + function buildQuizPoolFromMap(md) { + if (!md || !Array.isArray(md.quizQuestions)) return []; + return md.quizQuestions + .filter((q) => q && String(q.text || '').trim()) + .map((q) => ({ text: String(q.text).trim(), answerTrue: !!q.answerTrue })); + } + + function shufflePreviewQuizQuestionsPlay(arr) { + const a = (arr || []).slice(); + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; + } + + /** ตรงกับ server clampQuizRoundQuestionCount — 1–50 ค่าเริ่ม 10 */ + function clampPreviewQuizRoundQuestionCount(settings) { + const v = Number(settings && settings.quizRoundQuestionCount); + if (!Number.isFinite(v)) return 10; + return Math.max(1, Math.min(50, Math.floor(v))); + } + + function finishPreviewQuizSessionPlay() { + previewQuizStep = 'done'; + playQuizPhaseLocal = null; + previewQuizCurrent = null; + playQuizPhaseEndsAt = 0; + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const qov = document.getElementById('quiz-game-overlay'); + if (qov) qov.classList.add('is-hidden'); + const mapPanel = document.getElementById('quiz-map-question-panel'); + if (mapPanel) { + mapPanel.classList.add('is-hidden'); + mapPanel.setAttribute('aria-hidden', 'true'); + } + if (isQuizQuestionMissionUiMapPlay()) { + quizQuestionMissionPhase = 'ended'; + applyQuizQuestionMissionPanelImages(); + const mission = quizQuestionMissionBuildPayload(); + showGauntletCrownMissionOverlay(mission); + return; + } + playQuizText = 'ครบทุกข้อในรอบทดสอบแล้ว'; + if (qov) qov.classList.remove('is-hidden'); + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] จบรอบ'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'จำนวนข้อต่อรอบใช้ค่าเดียวกับ Admin (quizRoundQuestionCount)'; + } + + function clearPreviewBotAnswerPaths() { + if (!playBotsEnabled()) return; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.botPath = []; + o.botAnswerWander = false; + }); + } + + function resetPreviewBotsQuizState() { + if (!playBotsEnabled()) return; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.quizCannotTrue = false; + o.quizCannotFalse = false; + o.botPath = []; + o.botAnswerWander = false; + }); + } + + function applyPreviewReadPhase(q, poolLen) { + clearPreviewBotAnswerPaths(); + previewQuizStep = 'read'; + playQuizPhaseLocal = 'read'; + previewQuizCurrent = q; + playQuizText = q.text; + playQuizPhaseEndsAt = Date.now() + previewQuizTiming.readMs; + playQuizQuestionIndex = previewQuizQIndex + 1; + playQuizQuestionTotal = Math.max(0, Number(poolLen) || 0); + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) { + phaseEl.textContent = '[ทดสอบ] อ่านคำถาม · ข้อ ' + (previewQuizQIndex + 1) + ' / ' + poolLen; + } + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'โซนทองบนแผนที่ = ข้อความคำถาม · ฟ้า = จริง · ชมพู = เท็จ'; + } + + function applyPreviewAnswerPhase() { + previewQuizStep = 'answer'; + playQuizPhaseLocal = 'answer'; + playQuizPhaseEndsAt = Date.now() + previewQuizTiming.answerMs; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] เดินไปโซน ถูก / ผิด'; + const leg = document.getElementById('quiz-play-legend'); + if (leg && previewQuizCurrent) { + leg.textContent = previewQuizCurrent.answerTrue + ? 'เฉลย (ทดสอบ): คำตอบที่ถูกคือ «จริง» — โซนสีฟ้า' + : 'เฉลย (ทดสอบ): คำตอบที่ถูกคือ «เท็จ» — โซนสีชมพู'; + } + previewBotsPrepareAnswerRound(); + } + + function applyPreviewBetweenPhase() { + clearPreviewBotAnswerPaths(); + previewQuizStep = 'between'; + playQuizPhaseLocal = null; + playQuizText = 'ข้อถัดไปกำลังจะมา…'; + playQuizPhaseEndsAt = Date.now() + previewQuizTiming.betweenMs; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] พักระหว่างข้อ'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'รอสักครู่แล้วไปข้อถัดไป (สับแล้วตามจำนวนข้อต่อรอบ)'; + } + + function quizCellOnPlay(grid, x, y) { + return !!(grid && grid[y] && grid[y][x] === 1); + } + + function spawnTileWalkablePlay(md, x, y) { + const w = md.width || 20; + const h = md.height || 15; + if (x < 0 || x >= w || y < 0 || y >= h) return false; + const row = md.objects && md.objects[y]; + if (row && row[x] === 1) return false; + return true; + } + + function spawnFootprintFitsPlay(md, anchorX, anchorY) { + if (!spawnTileWalkablePlay(md, anchorX, anchorY)) return false; + const cellsW = Math.max(1, Math.min(4, Math.floor(Number(md.characterCellsW) || Number(md.characterCells) || 1))); + const cellsH = Math.max(1, Math.min(4, Math.floor(Number(md.characterCellsH) || Number(md.characterCells) || 1))); + const w = md.width || 20; + const h = md.height || 15; + const maxX = Math.min(w, anchorX + cellsW); + const maxY = Math.min(h, anchorY + cellsH); + for (let ty = anchorY; ty < maxY; ty++) { + for (let tx = anchorX; tx < maxX; tx++) { + if (!spawnTileWalkablePlay(md, tx, ty)) return false; + } + } + return true; + } + + /** เหมือน server pickRandomSpawnFromMap — สุ่มด้วย crypto.getRandomValues */ + function pickRandomSpawnFromMapPlay(md) { + const fallback = md.spawn || { x: 1, y: 1 }; + const fx = Number.isFinite(Number(fallback.x)) ? Number(fallback.x) : 1; + const fy = Number.isFinite(Number(fallback.y)) ? Number(fallback.y) : 1; + const grid = md.spawnArea; + if (!grid || !Array.isArray(grid)) return { x: fx, y: fy }; + const w = md.width || 20; + const h = md.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === 1 && spawnFootprintFitsPlay(md, x, y)) pool.push({ x, y }); + } + } + if (!pool.length) return { x: fx, y: fy }; + const u = new Uint32Array(1); + (typeof crypto !== 'undefined' && crypto.getRandomValues) + ? crypto.getRandomValues(u) + : (u[0] = (Math.floor(Math.random() * 0xffffffff) >>> 0)); + const pick = pool[u[0] % pool.length]; + return { x: pick.x, y: pick.y }; + } + + function parseLobbyPlayerSpawnsFromMapPlay(md) { + const w = md.width || 20; + const h = md.height || 15; + const out = [null, null, null, null, null, null]; + const raw = md && md.lobbyPlayerSpawns; + if (!Array.isArray(raw)) return out; + for (let i = 0; i < 6 && i < raw.length; i++) { + const cell = raw[i]; + if (!cell || typeof cell !== 'object') continue; + const x = Math.floor(Number(cell.x)); + const y = Math.floor(Number(cell.y)); + if (!Number.isFinite(x) || !Number.isFinite(y)) continue; + if (x < 0 || x >= w || y < 0 || y >= h) continue; + out[i] = { x, y }; + } + return out; + } + + /** jump_survive: ช่อง P1–P6 ที่วาดแบบยาน (shooterSpawnSlots) เติมใน slots6 เมื่อ lobbyPlayerSpawns ว่าง */ + function augmentLobbySlotsFromShooterPaintJumpSurvivePlay(md, slots6) { + if (!md || md.gameType !== 'jump_survive' || !Array.isArray(slots6)) return; + const g = md.shooterSpawnSlots; + if (!g) return; + const w = md.width || 20, h = md.height || 15; + for (let slot = 1; slot <= 6; slot++) { + const idx = slot - 1; + if (slots6[idx]) continue; + let found = false; + for (let yy = 0; yy < h && !found; yy++) { + const row = g[yy]; + if (!row) continue; + for (let xx = 0; xx < w; xx++) { + if (row[xx] === slot) { + slots6[idx] = { x: xx, y: yy }; + found = true; + break; + } + } + } + } + } + + /** สอดคล้องกับ server pickSpawnForJoin — ใช้พรีวิวบอท / ทดสอบ */ + function pickSpawnForJoinPlay(md, joinOrderIndex) { + if (!md) return { x: 1, y: 1 }; + const mode = md.lobbySpawnMode; + const ord = joinOrderIndex | 0; + if (mode === 'slots6' && ord >= 6) return pickRandomSpawnFromMapPlay(md); + const j = Math.min(Math.max(0, ord), 5); + if (mode === 'fixed' && md.spawn) { + const fx = Number.isFinite(Number(md.spawn.x)) ? Math.floor(Number(md.spawn.x)) : 1; + const fy = Number.isFinite(Number(md.spawn.y)) ? Math.floor(Number(md.spawn.y)) : 1; + const w = md.width || 20; + const h = md.height || 15; + const x = Math.max(0, Math.min(w - 1, fx)); + const y = Math.max(0, Math.min(h - 1, fy)); + if (spawnFootprintFitsPlay(md, x, y)) return { x, y }; + return pickRandomSpawnFromMapPlay(md); + } + if (mode === 'slots6') { + const slots = parseLobbyPlayerSpawnsFromMapPlay(md); + augmentLobbySlotsFromShooterPaintJumpSurvivePlay(md, slots); + const pick = slots[j]; + if (pick && spawnFootprintFitsPlay(md, pick.x, pick.y)) return { x: pick.x, y: pick.y }; + return pickRandomSpawnFromMapPlay(md); + } + return pickRandomSpawnFromMapPlay(md); + } + + const GAUNTLET_PREVIEW_MAX = 6; + function gauntletSpawnYsPlay(md, playerCount) { + const h = md.height || 15; + const lo = 1; + const hi = Math.max(lo, h - 2); + const n = Math.min(GAUNTLET_PREVIEW_MAX, Math.max(1, playerCount)); + if (hi <= lo) return Array.from({ length: n }, () => lo); + const ys = []; + for (let i = 0; i < n; i++) { + ys.push(Math.round(lo + (i * (hi - lo)) / Math.max(1, n - 1))); + } + return ys; + } + function collectGauntletSpawnSlotsFromSpawnAreaPlay(md) { + const grid = md.spawnArea; + if (!grid || !Array.isArray(grid)) return []; + const w = md.width || 20; + const h = md.height || 15; + const slots = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === 1 && spawnTileWalkablePlay(md, x, y)) slots.push({ x, y }); + } + } + slots.sort((a, b) => a.y - b.y || a.x - b.x); + return slots; + } + function collectGauntletSpawnSlotsPlay(md) { + const explicit = md.gauntletPlayerSpawns; + if (Array.isArray(explicit) && explicit.length > 0) { + const w = md.width || 20; + const h = md.height || 15; + const slots = []; + for (const raw of explicit) { + if (slots.length >= GAUNTLET_PREVIEW_MAX) break; + const x = Math.floor(Number(raw && raw.x)); + const y = Math.floor(Number(raw && raw.y)); + if (!Number.isFinite(x) || !Number.isFinite(y) || x < 0 || x >= w || y < 0 || y >= h) continue; + if (!spawnTileWalkablePlay(md, x, y)) continue; + slots.push({ x, y }); + } + if (slots.length > 0) return slots; + } + return collectGauntletSpawnSlotsFromSpawnAreaPlay(md); + } + function gauntletResolveSpawnXForRowPlay(md, spawnColX, y, slotXFallback) { + const w = md.width || 20; + if (spawnTileWalkablePlay(md, spawnColX, y)) return spawnColX; + if (slotXFallback != null && spawnTileWalkablePlay(md, slotXFallback, y)) return slotXFallback; + for (let d = 0; d < w; d++) { + if (spawnColX + d < w && spawnTileWalkablePlay(md, spawnColX + d, y)) return spawnColX + d; + if (spawnColX - d >= 0 && spawnTileWalkablePlay(md, spawnColX - d, y)) return spawnColX - d; + } + return Math.max(0, Math.min(w - 1, spawnColX)); + } + function gauntletSpawnPositionsPlay(md, playerCount) { + const n = Math.min(GAUNTLET_PREVIEW_MAX, Math.max(1, playerCount)); + const slots = collectGauntletSpawnSlotsPlay(md); + const fallbackYs = gauntletSpawnYsPlay(md, n); + const spawnColX = slots.length ? Math.min(...slots.map((s) => s.x)) : 1; + const out = []; + for (let i = 0; i < n; i++) { + const y = i < slots.length + ? slots[i].y + : (fallbackYs[i] != null ? fallbackYs[i] : fallbackYs[fallbackYs.length - 1]); + const slotX = i < slots.length ? slots[i].x : null; + const x = gauntletResolveSpawnXForRowPlay(md, spawnColX, y, slotX); + out.push({ x, y }); + } + return out; + } + /** + * โหมดทดสอบพรมแดง: จัดตำแหน่งเกิดให้ตรง server (spawnArea หรือกระจาย y) + * @param {boolean} [onlyBots=false] ถ้า true = จัดแค่บอท (หลัง game-start; ไม่ทับ me/คนจริงจาก peersSnap) + */ + function applyGauntletPreviewSpawnLayout(onlyBots) { + if (!mapData || mapData.gameType !== 'gauntlet') return; + const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + const humanCount = 1 + realIds.length; + const total = Math.min(GAUNTLET_PREVIEW_MAX, humanCount + botIds.length); + const pos = gauntletSpawnPositionsPlay(mapData, total); + if (onlyBots) { + let idx = humanCount; + botIds.forEach((bid) => { + const o = others.get(bid); + if (!o || idx >= pos.length) return; + o.x = pos[idx].x; + o.tx = pos[idx].x; + o.y = pos[idx].y; + o.ty = pos[idx].y; + idx++; + }); + return; + } + let idx = 0; + if (idx < pos.length) { + me.x = pos[idx].x; + me.y = pos[idx].y; + me.tx = me.x; + me.ty = me.y; + idx++; + } + realIds.forEach((rid) => { + const o = others.get(rid); + if (!o || idx >= pos.length) return; + o.x = pos[idx].x; + o.tx = pos[idx].x; + o.y = pos[idx].y; + o.ty = pos[idx].y; + idx++; + }); + botIds.forEach((bid) => { + const o = others.get(bid); + if (!o || idx >= pos.length) return; + o.x = pos[idx].x; + o.tx = pos[idx].x; + o.y = pos[idx].y; + o.ty = pos[idx].y; + idx++; + }); + } + + function isPreviewBotId(id) { + return typeof id === 'string' && id.indexOf(PREVIEW_BOT_PREFIX) === 0; + } + + function countPlayHumans() { + let n = 1; + others.forEach((_, id) => { if (!isPreviewBotId(id)) n++; }); + return n; + } + + function rebalancePreviewBots() { + if (!playBotsEnabled() || !mapData) return; + const human = countPlayHumans(); + const wantBots = Math.max(0, playBotTargetHeadcount() - human); + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o) return; + if (o.botTier == null) { + const tierRoll = Math.random(); + const stackPrev = mapData && mapData.gameType === 'stack'; + o.botTier = stackPrev + ? (tierRoll < 0.4 ? 'sharp' : tierRoll < 0.78 ? 'avg' : 'weak') + : (tierRoll < 0.28 ? 'sharp' : tierRoll < 0.62 ? 'avg' : 'weak'); + } + if (o.quizCannotTrue == null) o.quizCannotTrue = false; + if (o.quizCannotFalse == null) o.quizCannotFalse = false; + if (!Array.isArray(o.botPath)) o.botPath = []; + if (o.botAnswerWander == null) o.botAnswerWander = false; + if (o.gauntletJumpTicks == null) o.gauntletJumpTicks = 0; + if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks; + if (o.gauntletScore == null) { + o.gauntletScore = (mapData && mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) ? 100 : 0; + } + if (o.gauntletEliminated == null) o.gauntletEliminated = false; + if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) { + const wd = [[0, -1], [0, 1], [-1, 0], [1, 0]][Math.floor(Math.random() * 4)]; + o.botWanderDx = wd[0]; + o.botWanderDy = wd[1]; + } + if (typeof o.botWanderNextTurn !== 'number') { + o.botWanderNextTurn = Date.now() + 400 + Math.floor(Math.random() * 900); + } + }); + let botIds = [...others.keys()].filter(isPreviewBotId); + while (botIds.length > wantBots) { + const drop = botIds.pop(); + if (drop) others.delete(drop); + } + botIds = [...others.keys()].filter(isPreviewBotId); + while (botIds.length < wantBots) { + const id = PREVIEW_BOT_PREFIX + (++previewBotSeq); + const joinIdx = countPlayHumans() + botIds.length; + let x; + let y; + if (mapData.gameType === 'jump_survive') { + const pos = jumpSurviveSpawnWorldFromJoinOrderPlay(mapData, joinIdx); + x = pos.x; + y = pos.y; + } else { + const sp = pickSpawnForJoinPlay(mapData, joinIdx); + const jx = (Math.random() - 0.5) * 0.4; + const jy = (Math.random() - 0.5) * 0.4; + x = sp.x + 0.5 + jx; + y = sp.y + 0.5 + jy; + } + if (mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData)) { + const pathSnap = snapPositionOntoQuizBattlePathIfNeeded(x, y); + x = pathSnap.x; + y = pathSnap.y; + } + const tierRoll = Math.random(); + const stackPrev = mapData && mapData.gameType === 'stack'; + const botTier = stackPrev + ? (tierRoll < 0.4 ? 'sharp' : tierRoll < 0.78 ? 'avg' : 'weak') + : (tierRoll < 0.28 ? 'sharp' : tierRoll < 0.62 ? 'avg' : 'weak'); + const wd = [[0, -1], [0, 1], [-1, 0], [1, 0]][Math.floor(Math.random() * 4)]; + const crownStart = mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay(); + others.set(id, { + x, y, tx: x, ty: y, + direction: ['down', 'up', 'left', 'right'][Math.floor(Math.random() * 4)], + nickname: 'บอท', + characterId: pickPreviewBotCharacterId(botIds.length), + spawnJoinOrder: joinIdx, + playTint: playTintFromPeerId(id), + botTier, + gauntletJumpTicks: 0, + gauntletJumpVis: 0, + gauntletScore: crownStart ? 100 : 0, + gauntletEliminated: false, + quizCannotTrue: false, + quizCannotFalse: false, + quizCarryHeld: null, + botPath: [], + botAnswerWander: false, + botWanderDx: wd[0], + botWanderDy: wd[1], + botWanderNextTurn: Date.now() + 400 + Math.floor(Math.random() * 1000), + jumpSurviveEliminated: false, + spaceShooterScore: 0, + balloonBossScore: 0, + balloonBossBossDmg: 0, + balloonBossBalloons: mapData && mapData.gameType === 'balloon_boss' ? balloonBossBalloonsStartPlay() : 5, + balloonBossEliminated: false, + botQuizCarryPathfindAfter: performance.now() + previewBotSeq * 75 + Math.floor(Math.random() * 160), + }); + botIds.push(id); + } + let k = 0; + [...others.keys()].filter(isPreviewBotId).sort().forEach((bid) => { + const o = others.get(bid); + if (!o) return; + const tag = o.botTier === 'sharp' ? '(ฉลาด)' : o.botTier === 'weak' ? '(พลาดบ่อย)' : '(กลาง)'; + o.nickname = 'บอท ' + (++k) + ' ' + tag; + }); + [...others.keys()].filter(isPreviewBotId).sort().forEach((bid, botIdx) => { + const o = others.get(bid); + if (o) o.characterId = pickPreviewBotCharacterId(botIdx); + }); + if (mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData)) { + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o) return; + const s = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y); + o.x = s.x; + o.y = s.y; + o.tx = s.x; + o.ty = s.y; + }); + } + if (mapData.gameType === 'quiz_carry') { + const t0 = performance.now(); + [...others.keys()].filter(isPreviewBotId).forEach((bid, idx) => { + const o = others.get(bid); + if (!o) return; + o.botQuizCarryPathfindAfter = t0 + idx * 90 + Math.floor(Math.random() * 140); + }); + } + if (mapData.gameType === 'gauntlet') { + applyGauntletPreviewSpawnLayout(false); + emitGauntletPreviewRowsToServer(); + } + if (mapData.gameType === 'stack') { + applyStackPreviewSpawnLayout(); + rebuildStackPreviewTurnOrder(); + } + if (mapData.gameType === 'jump_survive') { + applyJumpSurvivePreviewSpawnLayout(false); + } + if (mapData.gameType === 'space_shooter') { + applySpaceShooterSpawnLayoutPlay(); + } + if (mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + } + if (quizCarryPregameActive && mapData && mapData.gameType === 'quiz_carry') updateQuizCarryPregameHud(); + } + + function pickRandomWalkableCellInAnswerGrid(grid, o) { + if (!grid || !mapData) return null; + const w = mapData.width || 20, h = mapData.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = grid[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] !== 1) continue; + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, NaN, NaN, o)) pool.push({ x, y }); + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + /** ช่วงตอบ: บอทสุ่มเดินไปโซนถูก/ผิด/เดินมั่ว ตามระดับ (ฉลาด/กลาง/พลาดบ่อย) */ + function previewBotsPrepareAnswerRound() { + if (!playBotsEnabled() || !mapData || !isQuiz() || !previewQuizCurrent) return; + const correctTrue = !!previewQuizCurrent.answerTrue; + const qt = mapData.quizTrueArea; + const qf = mapData.quizFalseArea; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.botPath = []; + o.botAnswerWander = false; + const tier = o.botTier || 'avg'; + let pCorrect = 0.52; + if (tier === 'sharp') pCorrect = 0.84; + else if (tier === 'weak') pCorrect = 0.22; + if (Math.random() < 0.14) { + o.botAnswerWander = true; + return; + } + const wantsCorrect = Math.random() < pCorrect; + const targetTrue = wantsCorrect ? correctTrue : !correctTrue; + const zoneGrid = targetTrue ? qt : qf; + const dest = pickRandomWalkableCellInAnswerGrid(zoneGrid, o); + if (!dest) { + o.botAnswerWander = true; + return; + } + const path = pathfindPlayForBot(o.x, o.y, dest.x + 0.5, dest.y + 0.5, o); + if (!path || path.length <= 1) { + o.botAnswerWander = true; + return; + } + o.botPath = path.slice(1); + }); + } + + /** คัดลอกตรรกะจาก server — ดีดออกนอกโซนจริง/เท็จ (โหมดทดสอบ preview บน play.html) */ + function findNearestOutsideQuizAnswerZonesPlay(md, sx, sy) { + const w = md.width || 20; + const h = md.height || 15; + const tGrid = md.quizTrueArea || []; + const fGrid = md.quizFalseArea || []; + const inAnswer = (x, y) => quizCellOnPlay(tGrid, x, y) || quizCellOnPlay(fGrid, x, y); + const walkable = (x, y) => { + if (x < 0 || x >= w || y < 0 || y >= h) return false; + const row = md.objects && md.objects[y]; + return row && row[x] !== 1; + }; + const fx = Math.floor(Number(sx)); + const fy = Math.floor(Number(sy)); + if (!walkable(fx, fy)) { + const sp = md.spawn || { x: 1, y: 1 }; + return { x: (typeof sp.x === 'number' ? sp.x : 1) + 0.5, y: (typeof sp.y === 'number' ? sp.y : 1) + 0.5 }; + } + if (!inAnswer(fx, fy)) return { x: sx, y: sy }; + const q = [[fx, fy]]; + const seen = new Set([`${fx},${fy}`]); + const dirs = [[0, 1], [0, -1], [1, 0], [-1, 0]]; + while (q.length) { + const [cx, cy] = q.shift(); + for (let di = 0; di < dirs.length; di++) { + const nx = cx + dirs[di][0]; + const ny = cy + dirs[di][1]; + const k = `${nx},${ny}`; + if (seen.has(k)) continue; + if (!walkable(nx, ny)) continue; + seen.add(k); + if (!inAnswer(nx, ny)) { + return { x: nx + 0.5, y: ny + 0.5 }; + } + q.push([nx, ny]); + } + } + const sp = md.spawn || { x: 1, y: 1 }; + return { x: (typeof sp.x === 'number' ? sp.x : 1) + 0.5, y: (typeof sp.y === 'number' ? sp.y : 1) + 0.5 }; + } + + function quizResolveChoiceFromStanding(ox, oy, correctTrue) { + const tx = Math.floor(ox); + const ty = Math.floor(oy); + const qt = mapData.quizTrueArea; + const qf = mapData.quizFalseArea; + const inT = quizCellOnPlay(qt, tx, ty); + const inF = quizCellOnPlay(qf, tx, ty); + let choice = null; + if (inT && !inF) choice = true; + else if (inF && !inT) choice = false; + else if (inT && inF) choice = true; + const right = choice !== null && choice === correctTrue; + return { choice, right }; + } + + /** ลำดับจุดเกิดเดียวกับ pickSpawnForJoinPlay — ห้ามสุ่มทั้ง spawnArea หลังตอบผิด (จะหลุดจาก P1–P6) */ + function quizPreviewJoinOrderForRespawn(actorId) { + if (actorId === myId) { + const jm = Number(me && me.spawnJoinOrder); + return Number.isFinite(jm) ? Math.max(0, Math.floor(jm)) : 0; + } + const o = others.get(actorId); + if (o && typeof o.spawnJoinOrder === 'number' && Number.isFinite(o.spawnJoinOrder)) { + return Math.max(0, Math.floor(o.spawnJoinOrder)); + } + if (!isPreviewBotId(actorId)) { + const othersReal = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + const idx = othersReal.indexOf(actorId); + return idx >= 0 ? 1 + idx : 1; + } + const bots = [...others.keys()].filter(isPreviewBotId).sort(); + const bi = bots.indexOf(actorId); + if (bi < 0) return 0; + return countPlayHumans() + bi; + } + + /** เฉลยรอบทดสอบบนเครื่อง — ให้พฤติกรรมใกล้เคียง server (ผิด = ล็อกโซน + ดีดออก) */ + function resolvePreviewQuizRound() { + if (!previewMode || !mapData || !isQuiz() || !previewQuizCurrent || myId == null) return; + const correctTrue = !!previewQuizCurrent.answerTrue; + const results = []; + const mine = quizResolveChoiceFromStanding(me.x, me.y, correctTrue); + let right = mine.right; + let choice = mine.choice; + if (mine.right) { + playLiveQuizScores[myId] = (playLiveQuizScores[myId] || 0) + QUIZ_TF_POINTS_PER_CORRECT; + } else { + playQuizPlayerLocal.cannotTrue = true; + playQuizPlayerLocal.cannotFalse = true; + const ordMe = quizPreviewJoinOrderForRespawn(myId); + const sp = pickSpawnForJoinPlay(mapData, ordMe); + const pos = findNearestOutsideQuizAnswerZonesPlay(mapData, sp.x + 0.5, sp.y + 0.5); + me.x = pos.x; + me.y = pos.y; + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + } + results.push({ + id: myId, + right, + choice, + score: playLiveQuizScores[myId] != null ? playLiveQuizScores[myId] : 0, + }); + if (playBotsEnabled()) { + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + const br = quizResolveChoiceFromStanding(o.x, o.y, correctTrue); + o.botPath = []; + o.botAnswerWander = false; + if (br.right) { + playLiveQuizScores[id] = (playLiveQuizScores[id] || 0) + QUIZ_TF_POINTS_PER_CORRECT; + } else { + o.quizCannotTrue = true; + o.quizCannotFalse = true; + const ordB = quizPreviewJoinOrderForRespawn(id); + const sp = pickSpawnForJoinPlay(mapData, ordB); + const pos = findNearestOutsideQuizAnswerZonesPlay(mapData, sp.x + 0.5, sp.y + 0.5); + o.x = pos.x + (Math.random() - 0.5) * 0.35; + o.y = pos.y + (Math.random() - 0.5) * 0.35; + } + results.push({ + id, + right: br.right, + choice: br.choice, + score: playLiveQuizScores[id] != null ? playLiveQuizScores[id] : 0, + }); + }); + } + const r = { results, scores: { ...playLiveQuizScores } }; + results.forEach(function (row) { + if (!row || row.id == null) return; + if (!row.right) playQuizEverWrong[String(row.id)] = true; + }); + renderPlayQuizScoreboard(playLiveQuizScores); + if (results.some(function (row) { return row && row.right; })) { + spawnQuizTrueFalseScorePopupPlay(correctTrue); + } + showPlayQuizFeedback(r); + } + + function advancePreviewQuizIfDue() { + if (isQuizQuestionMissionUiMapPlay() && isQuizQuestionMissionPregameBlockingPlay()) return; + if (previewQuizStep === 'done') return; + if (!previewMode || !isQuiz() || !playQuizPhaseEndsAt || Date.now() < playQuizPhaseEndsAt) return; + const pool = previewQuizPool; + if (!pool || !pool.length) return; + if (previewQuizStep === 'read') { + applyPreviewAnswerPhase(); + return; + } + if (previewQuizStep === 'answer') { + resolvePreviewQuizRound(); + applyPreviewBetweenPhase(); + return; + } + if (previewQuizStep === 'between') { + previewQuizQIndex += 1; + if (previewQuizQIndex >= pool.length) { + finishPreviewQuizSessionPlay(); + return; + } + const q = pool[previewQuizQIndex]; + if (q) applyPreviewReadPhase(q, pool.length); + } + } + + async function loadPreviewQuizAndStart() { + resetPreviewBotsQuizState(); + playQuizEverWrong = {}; + let settings = null; + try { + const r = await fetch(BASE + '/api/quiz-settings?_=' + Date.now(), { cache: 'no-store' }); + if (r.ok) settings = await r.json(); + } catch (e) { /* use map fallback */ } + let pool = []; + if (settings && Array.isArray(settings.questions) && settings.questions.length) { + pool = settings.questions + .filter((q) => q && String(q.text || '').trim()) + .map((q) => ({ text: String(q.text).trim(), answerTrue: !!q.answerTrue })); + } + if (!pool.length && mapData) pool = buildQuizPoolFromMap(mapData); + const cap = clampPreviewQuizRoundQuestionCount(settings); + const shuffled = shufflePreviewQuizQuestionsPlay(pool); + previewQuizPool = shuffled.slice(0, Math.min(cap, shuffled.length)); + previewQuizQIndex = 0; + const dRead = 10000; + const dAns = 5000; + const dBet = 3500; + previewQuizTiming = { + readMs: clampPreviewMs(settings && settings.readMs, dRead, 1000, 300000), + answerMs: clampPreviewMs(settings && settings.answerMs, dAns, 1000, 300000), + betweenMs: clampPreviewMs(settings && settings.betweenMs, dBet, 0, 300000), + }; + previewQuizCurrent = null; + if (!pool.length) { + playQuizPhaseLocal = 'read'; + previewQuizStep = 'read'; + playQuizText = 'ยังไม่มีคำถามในชุด — ตั้งคำถามใน Admin → คำถามเกม หรือในแมป (quizQuestions)'; + playQuizPhaseEndsAt = 0; + playQuizQuestionIndex = 0; + playQuizQuestionTotal = 0; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] ไม่มีคำถามในระบบ'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const leg = document.getElementById('quiz-play-legend'); + if (leg) leg.textContent = 'โซนสีบนแผนที่ยังใช้ดูเลย์เอาต์ได้ตามปกติ'; + initPlayLiveQuizScoresZeros(); + startPlayQuizTimer(); + return; + } + const session = previewQuizPool; + const q = session[0]; + if (q) applyPreviewReadPhase(q, session.length); + initPlayLiveQuizScoresZeros(); + startPlayQuizTimer(); + } + + function startPlayQuizTimer() { + if (playQuizTimerInterval) clearInterval(playQuizTimerInterval); + playQuizTimerInterval = setInterval(() => { + updatePlayQuizTimerDisplay(); + advancePreviewQuizIfDue(); + }, 200); + updatePlayQuizTimerDisplay(); + } + + function teardownPlayQuizUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) ov.classList.add('is-hidden'); + const panel = document.getElementById('quiz-map-question-panel'); + const mapQText = document.getElementById('quiz-map-question-text'); + if (panel) { + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + clearQuizMapPanelThemeInline(panel, mapQText); + } + playQuizPhaseLocal = null; + playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + playQuizText = ''; + playQuizPhaseEndsAt = 0; + playQuizQuestionIndex = 0; + playQuizQuestionTotal = 0; + previewQuizPool = []; + previewQuizQIndex = 0; + previewQuizCurrent = null; + previewQuizStep = 'read'; + playLiveQuizScores = {}; + playQuizEverWrong = {}; + quizTfScorePopups = []; + playPath = []; + hidePlayQuizHud(); + quizCarryJoinSettingsSnap = null; + quizCarryWalkSpeedMultActive = QUIZ_CARRY_WALK_SPEED_MULT; + quizCarryMapPanelTheme = null; + quizMapPanelTheme = null; + quizCarryChoicePlaqueThemes = null; + resetQuizCarryPlayState(); + resetQuizBattlePlayState(); + quizQuestionMissionPhase = null; + quizQuestionMissionDeferredPhase = null; + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + const gcmTeardown = document.getElementById('gauntlet-crown-mission-overlay'); + if (gcmTeardown) gcmTeardown.classList.add('is-hidden'); + cancelEmbedPreviewLobbyReturnTimer(); + const grabBtnTeardown = document.getElementById('quiz-carry-grab-btn'); + if (grabBtnTeardown) { + grabBtnTeardown.classList.add('is-hidden'); + grabBtnTeardown.classList.remove('quiz-carry-grab-btn--active'); + grabBtnTeardown.classList.remove('quiz-carry-grab-btn--place'); + grabBtnTeardown.style.pointerEvents = ''; + const im = grabBtnTeardown.querySelector('img'); + if (im) im.src = '/Game/img/quiz-carry/btn-grab.png'; + } + const gchJumpTeardown = document.getElementById('gauntlet-crown-jump-btn'); + if (gchJumpTeardown) { + gchJumpTeardown.classList.add('is-hidden'); + gchJumpTeardown.setAttribute('aria-hidden', 'true'); + gchJumpTeardown.style.pointerEvents = ''; + } + const stackDropTeardown = document.getElementById('stack-tower-drop-btn'); + if (stackDropTeardown) { + stackDropTeardown.classList.add('is-hidden'); + stackDropTeardown.setAttribute('aria-hidden', 'true'); + stackDropTeardown.style.pointerEvents = ''; + stackDropTeardown.classList.remove('stack-tower-drop-btn--active'); + } + hideStackTowerResultFlashDomOnlyPlay(); + } + + function setupPlayQuizUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) { + const hideBottomQuizBar = (previewMode && editorEmbedReturn && !isQuizCarry()) + || (isQuiz() && isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'live'); + if (hideBottomQuizBar) ov.classList.add('is-hidden'); + else ov.classList.remove('is-hidden'); + } + playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + const leg = document.getElementById('quiz-play-legend'); + if (previewMode) { + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = '[ทดสอบ] กำลังโหลดคำถาม…'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = 'ดึงชุดคำถามจาก /api/quiz-settings (หรือจากแมป)…'; + if (leg) leg.textContent = ''; + if (isQuizQuestionMissionUiMapPlay()) { + if (phaseEl) phaseEl.textContent = '[ทดสอบ] รอกด READY เพื่อเริ่มภารกิจ'; + if (qEl) qEl.textContent = 'โหมดภารกิจ — หน้าปก HOWTO จาก /img/QUESTION/'; + if (leg) leg.textContent = ''; + } else { + loadPreviewQuizAndStart(); + } + } else { + playQuizPhaseLocal = null; + playQuizText = 'รอคำถามจากโฮสต์…'; + playQuizPhaseEndsAt = 0; + playQuizQuestionIndex = 0; + playQuizQuestionTotal = 0; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'แมพตอบคำถาม'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = 'เข้าร่วมห้องแล้ว — เมื่อโฮสต์เริ่มเกม ข้อความและเวลาจะอัปเดตที่นี่ (เล่นจริงแนะนำผ่าน room-lobby)'; + if (leg) leg.textContent = 'โซนสีบนแผนที่คือจุดตอบเหมือนใน Lobby'; + const tEl = document.getElementById('quiz-game-timer'); + if (tEl) tEl.textContent = ''; + } + if (isQuiz() && !isQuizCarry()) { + (async () => { + try { + let s = {}; + const r = await fetch(BASE + '/api/quiz-settings?_=' + Date.now(), { cache: 'no-store' }); + if (r.ok) { + const raw = await r.text(); + const trimmed = (raw || '').trim(); + if (trimmed.startsWith('{') && trimmed.endsWith('}')) { + try { + s = JSON.parse(trimmed); + } catch (e) { /* ignore */ } + } + } + s = await mergeQuizCarrySettingsFromDisk(s && typeof s === 'object' ? s : {}); + setQuizMapPanelThemeFromApi(s); + try { + syncPlayQuizMapPanel(); + } catch (e2) { /* ignore */ } + } catch (e) { /* ignore */ } + })(); + } + } + + /** อัปเดตแถบคำถาม quiz จากเซิร์ฟเวอร์ — แยกไว้ให้ภารกิจ mng8a80o เรียกหลังจบ countdown */ + function applyQuizPhaseFromServer(p) { + if (!p || !mapData || !isQuiz()) return; + if (p.text) playQuizText = p.text; + playQuizPhaseLocal = p.phase; + playQuizPhaseEndsAt = p.endsAt || 0; + playQuizQuestionIndex = Math.max(0, Number(p.questionIndex) || 0); + playQuizQuestionTotal = Math.max(0, Number(p.questionTotal) || 0); + const phaseEl = document.getElementById('quiz-game-phase-label'); + const qEl = document.getElementById('quiz-game-question'); + if (phaseEl) { + phaseEl.textContent = p.phase === 'read' + ? ('อ่านคำถาม ข้อ ' + (p.questionIndex || '') + '/' + (p.questionTotal || '')) + : ('เดินไปโซน ถูก / ผิด — ข้อ ' + (p.questionIndex || '') + '/' + (p.questionTotal || '')); + } + if (qEl) qEl.textContent = playQuizText || ''; + const ov = document.getElementById('quiz-game-overlay'); + if (isQuizQuestionMissionHudActivePlay()) { + if (ov) ov.classList.add('is-hidden'); + } else if (ov) { + ov.classList.remove('is-hidden'); + } + if (playQuizTimerInterval) clearInterval(playQuizTimerInterval); + playQuizTimerInterval = setInterval(updatePlayQuizTimerDisplay, 200); + updatePlayQuizTimerDisplay(); + if (!previewMode && isQuiz() && !Object.keys(playLiveQuizScores).length) initPlayLiveQuizScoresZeros(); + if (!previewMode && isQuiz() && p.phase === 'read' && Number(p.questionIndex) === 1) playQuizEverWrong = {}; + try { + syncPlayQuizMapPanel(); + } catch (eQm) { /* ignore */ } + } + + function isFrogger() { return mapData && mapData.gameType === 'frogger'; } + function isGauntlet() { return mapData && mapData.gameType === 'gauntlet'; } + function isGauntletFaceRightMapMno9kb07() { + if (!isGauntlet()) return false; + /** ใช้ currentPlayMapId (session → mapData.id → ?map=) ให้สอดคล้อง runway / timing — กัน HUD crown หลุดค้างเป็นแผงเก่าแนวตั้งทับ TIME */ + return (currentPlayMapId() || '').trim() === GAUNTLET_FACE_RIGHT_MAP_ID; + } + /** Crown heist rules + UI (เดียวกับหันขวา — ฉาก mno9kb07) */ + function isGauntletCrownHeistMapPlay() { + return isGauntletFaceRightMapMno9kb07(); + } + function isMegaVirusMissionShellMapPlay() { + return !!(mapData && mapData.gameType === 'balloon_boss' && currentPlayMapId() === BALLOON_BOSS_MISSION_MAP_ID); + } + /** Crown gauntlet หรือ Mega Virus — lobby HOWTO / Ready / นับถอยหลัง / held run */ + function usesCrownLobbyShellPlay() { + return isGauntletCrownHeistMapPlay() || isMegaVirusMissionShellMapPlay(); + } + /** Last Light (mno9kb07) embed — ค่าเริ่มต้นเมื่อโหลดแมป (ปรับซูมได้ด้วยล้อ / [ ]) */ + const PLAY_EMBED_LAST_LIGHT_DEFAULT_ZOOM_MUL = 1.49; + /** Stack Tower (mnn93hpi) ใน embed พรีวิว: ซูมคงที่ ×1 — ล้อ/ปุ่มไม่ปรับได้ */ + const PLAY_EMBED_STACK_TOWER_FIXED_ZOOM_MUL = 1; + function isStackTowerEmbedZoomLockedPlay() { + return !!(previewMode && editorEmbedReturn && mapData && isStackTowerMissionUiMapPlay()); + } + function applyPlayEmbedZoomForCurrentMapPlay() { + if (isStackTowerEmbedZoomLockedPlay()) { + playEmbedUserZoomMul = PLAY_EMBED_STACK_TOWER_FIXED_ZOOM_MUL; + } else if (previewMode && editorEmbedReturn && mapData && isGauntletCrownHeistMapPlay()) { + playEmbedUserZoomMul = PLAY_EMBED_LAST_LIGHT_DEFAULT_ZOOM_MUL; + } + } + + /** smoothstep 0..1 */ + function stackTowerPost50SmoothstepPlay(t) { + const x = Math.max(0, Math.min(1, t)); + return x * x * (3 - 2 * x); + } + + /** Tower live + progress ≥เกณฑ์: 0→1 ระหว่าง STACK_TOWER_POST50_ANIM_MS — ใช้เฉพาะเมื่อเปิดกล้องตามหอ (ไม่งั้นฉากธรรมดาไม่เลื่อน/ซูมหลังเกณฑ์) */ + function getStackTowerPost50AnimEasePlay() { + if (!getStackTowerCameraFollowPlayConfig().enabled) { + stackTowerPost50AnimStartMs = null; + return 0; + } + if (!isStackTowerMissionUiMapPlay() || stackTowerMissionPhase !== 'live' || !stackMini) return 0; + const p = Number(stackMini.progressPct) || 0; + if (p < STACK_TOWER_POST50_PROGRESS_THRESH) { + stackTowerPost50AnimStartMs = null; + return 0; + } + if (stackTowerPost50AnimStartMs == null) stackTowerPost50AnimStartMs = performance.now(); + const u = Math.min(1, Math.max(0, (performance.now() - stackTowerPost50AnimStartMs) / STACK_TOWER_POST50_ANIM_MS)); + return stackTowerPost50SmoothstepPlay(u); + } + + function getStackTowerPost50ZMulPlay() { + const e = getStackTowerPost50AnimEasePlay(); + if (e <= 0) return 1; + return 1 + (STACK_TOWER_POST50_ZOOM_MUL - 1) * e; + } + + function getStackTowerPost50BgScrollExtraPxPlay(ch) { + const e = getStackTowerPost50AnimEasePlay(); + if (e <= 0) return 0; + const chR = Math.max(1, Number(ch) || 720); + return STACK_TOWER_POST50_MAP_SHIFT_SCREEN_FRAC * chR * e; + } + + /** + * Same zDraw chain as draw() (zoom, special maps, Last Light embed lock, preview embed mul). + * Runway end checks must not rely on lastPlayZDrawForInput alone — it can lag map/canvas or default 1.4 vs real zoom. + */ + function computePlayCameraZDrawPlay() { + if (!canvas || !mapData) return zoom; + const w = mapData.width || 20; + const h = mapData.height || 15; + let zDraw = zoom; + if (isSpaceShooter() || isBalloonBoss()) { + const mwPx = w * tileSize; + const mhPx = h * tileSize; + zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96; + } else if (isQuizQuestionMissionHudActivePlay()) { + const qmc = getQuizQuestionMissionMapCenterWorldPxPlay(); + if (qmc) { + zDraw = Math.min(canvas.width / qmc.mwPx, canvas.height / qmc.mhPx) * 0.96; + } + } else if (isQuizCarry()) { + const mwPx = w * tileSize; + const mhPx = h * tileSize; + zDraw = Math.min(canvas.width / mwPx, canvas.height / mhPx) * 0.96; + } + if (isStackTowerEmbedZoomLockedPlay()) { + playEmbedUserZoomMul = PLAY_EMBED_STACK_TOWER_FIXED_ZOOM_MUL; + } + if (previewMode && editorEmbedReturn && mapData && !isQuizQuestionMissionHudActivePlay()) { + zDraw *= playEmbedUserZoomMul; + } + if (isStackTowerMissionUiMapPlay()) { + zDraw *= STACK_TOWER_FIXED_RENDER_Z_MUL; + zDraw *= getStackTowerPost50ZMulPlay(); + } + return zDraw; + } + let gauntletCrownHowtoVisible = false; + function isGauntletCrownPregameBlockingPlay() { + if (!usesCrownLobbyShellPlay()) return false; + if (gauntletCrownRunHeldRemote) return true; + if (gauntletCrownPregamePhase != null && gauntletCrownPregamePhase !== 'live') return true; + return false; + } + + /** + * Last Light (crown heist): center camera on the whole party bbox instead of only `me` + * — reduces empty void on the side when you fall behind and keeps bots + you in frame together when possible. + */ + function getGauntletCrownHeistGroupCameraCenterPxPlay(tileSizeRef, canvasW, canvasH, zDrawVal) { + if (!isGauntletCrownHeistMapPlay() || !mapData) return null; + const md = mapData; + const { cw, ch } = getCharacterFootprintWH(md); + const ww = md.width || 20; + const hh = md.height || 15; + const mapWpx = ww * tileSizeRef; + const mapHpx = hh * tileSizeRef; + const halfW = canvasW / (2 * zDrawVal); + const halfH = canvasH / (2 * zDrawVal); + const ents = []; + if (!me.gauntletEliminated && Number.isFinite(me.x) && Number.isFinite(me.y)) { + ents.push({ cx: (me.x + cw * 0.5) * tileSizeRef, cy: (me.y + ch * 0.5) * tileSizeRef }); + } + others.forEach((o, id) => { + if (!o || o.gauntletEliminated) return; + if (!playBotsEnabled() && isPreviewBotId(id)) return; + if (!Number.isFinite(o.x) || !Number.isFinite(o.y)) return; + ents.push({ cx: (o.x + cw * 0.5) * tileSizeRef, cy: (o.y + ch * 0.5) * tileSizeRef }); + }); + if (!ents.length) return null; + let minCx = Infinity; + let maxCx = -Infinity; + let sumCy = 0; + for (let i = 0; i < ents.length; i++) { + const e = ents[i]; + minCx = Math.min(minCx, e.cx); + maxCx = Math.max(maxCx, e.cx); + sumCy += e.cy; + } + let px = (minCx + maxCx) * 0.5; + let py = sumCy / ents.length; + const minCamX = halfW; + const maxCamX = Math.max(minCamX, mapWpx - halfW); + const minCamY = halfH; + const maxCamY = Math.max(minCamY, mapHpx - halfH); + const viewW = halfW * 2; + const viewH = halfH * 2; + /* + * ถ้ามุมมองกว้าง/สูงกว่าแมป: อย่าใช้ clamp แบบ minCamX (= กล้องชิดซ้ายโลก) — จะดูเหมือนแมปติดมุมซ้ายบน (เด่นใน embed / ซูมออก) + * ให้จัดกลางแมปแทน — ภารกิจคำถาม mng8a80o ช่วง live ใช้กล้องกลางแมปเหมือนกัน (ดู isQuizQuestionMissionHudActivePlay + draw) + */ + if (viewW >= mapWpx) px = mapWpx * 0.5; + else px = Math.max(minCamX, Math.min(maxCamX, px)); + if (viewH >= mapHpx) py = mapHpx * 0.5; + else py = Math.max(minCamY, Math.min(maxCamY, py)); + return { px, py }; + } + + /** Preview bots only — full tail-off loss needs server support for real humans */ + const GAUNTLET_CROWN_TAIL_OFFSCREEN_MARGIN_PX = 40; + function maybeEliminateGauntletPreviewBotsTailOffCameraPlay(camX, camY, zDrawVal) { + if (!playBotsEnabled() || !isGauntletCrownHeistMapPlay() || !mapData) return; + if (gauntletCrownPregamePhase !== 'live') return; + const halfW = canvas.width / (2 * zDrawVal); + const worldMinX = camX - halfW; + const gate = worldMinX - GAUNTLET_CROWN_TAIL_OFFSCREEN_MARGIN_PX; + const { cw } = getCharacterFootprintWH(mapData); + const ts = mapData.tileSize || 32; + others.forEach((o, id) => { + if (!isPreviewBotId(id) || !o || o.gauntletEliminated) return; + const rightWorld = (Number(o.x) + cw) * ts; + if (rightWorld < gate) o.gauntletEliminated = true; + }); + } + + function isStack() { return mapData && mapData.gameType === 'stack'; } + function isJumpSurvive() { return mapData && mapData.gameType === 'jump_survive'; } + + function currentPlayMapId() { + const q = (playSessionMapId || '').trim(); + if (q) return q; + if (mapData && mapData.id != null && String(mapData.id).trim() !== '') return String(mapData.id).trim(); + return (playMapIdFromQuery || '').trim(); + } + + function isQuizQuestionMissionUiMapPlay() { + return isQuiz() && currentPlayMapId() === QUIZ_QUESTION_MISSION_MAP_ID; + } + + function isQuizQuestionMissionPregameBlockingPlay() { + if (!isQuizQuestionMissionUiMapPlay()) return false; + return quizQuestionMissionPhase != null && quizQuestionMissionPhase !== 'live' && quizQuestionMissionPhase !== 'ended'; + } + + function isStackTowerMissionUiMapPlay() { + return isStack() && currentPlayMapId() === STACK_TOWER_MISSION_MAP_ID; + } + + function getStackTowerBlockWorldScalePlay() { + return isStackTowerMissionUiMapPlay() ? STACK_TOWER_BLOCK_WORLD_SCALE : 1; + } + + function isStackTowerMissionPregameBlockingPlay() { + if (!isStackTowerMissionUiMapPlay()) return false; + return stackTowerMissionPhase != null && stackTowerMissionPhase !== 'live' && stackTowerMissionPhase !== 'ended'; + } + + function isStackTowerMissionHudActivePlay() { + return isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'live'; + } + + /** mnptfts2 Jumper — ใช้ cyber HUD แบบภารกิจคำถาม (TIME plaque + SCORE แถว QM + กรอบโปรไฟล์) */ + function isJumpSurviveMissionHudActivePlay() { + return isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'live'; + } + + /** Space Shooter mnpz6rkp — ช่วงเล่นจริง: HUD เดียวกับ Stack mission (คลาส question-mission + TIME plaque) */ + function isSpaceShooterMissionHudActivePlay() { + return isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'live'; + } + + function stackTowerAssetUrl(file) { + return BASE + '/img/TowerBlock/' + String(file || '').replace(/^\/+/, ''); + } + + function ensureStackTowerSlingImagePlay() { + if (!stackTowerSlingImg) { + stackTowerSlingImg = new Image(); + stackTowerSlingImg.decoding = 'async'; + stackTowerSlingImg.src = stackTowerAssetUrl('sling.png'); + } + const im = stackTowerSlingImg; + return im && im.complete && im.naturalWidth > 0 ? im : null; + } + + function ensureStackTowerScorePopupImagesPlay() { + if (!stackTowerScorePlus10Img) { + stackTowerScorePlus10Img = new Image(); + stackTowerScorePlus10Img.decoding = 'async'; + stackTowerScorePlus10Img.src = stackTowerAssetUrl('score+10.png'); + } + if (!stackTowerScorePlus20Img) { + stackTowerScorePlus20Img = new Image(); + stackTowerScorePlus20Img.decoding = 'async'; + stackTowerScorePlus20Img.src = stackTowerAssetUrl('score+20.png'); + } + return { p10: stackTowerScorePlus10Img, p20: stackTowerScorePlus20Img }; + } + + function ensureStackTowerPerfectFxImagesPlay() { + if (!stackTowerScoreLightImg) { + stackTowerScoreLightImg = new Image(); + stackTowerScoreLightImg.decoding = 'async'; + stackTowerScoreLightImg.src = stackTowerAssetUrl('score-light.png'); + } + if (!stackTowerScoreX2Img) { + stackTowerScoreX2Img = new Image(); + stackTowerScoreX2Img.decoding = 'async'; + stackTowerScoreX2Img.src = stackTowerAssetUrl('scorex2.png'); + } + return { light: stackTowerScoreLightImg, x2: stackTowerScoreX2Img }; + } + + /** score-light หลัง scorex2 — วาดเป็นคู่ข้างชั้นบน (mock รูป2) */ + function drawStackTowerPerfectX2ClusterPlay(ctx, lightImg, x2Img, sx, sy, drawW, drawH, nowT, fxUntil) { + if (!x2Img || !x2Img.complete || !x2Img.naturalWidth || drawW <= 0 || drawH <= 0) return; + const iw = x2Img.naturalWidth; + const ih = x2Img.naturalHeight; + const targetH = Math.max(drawH * 0.92, Math.min(drawH * 1.35, 72)); + const scale = targetH / ih; + const dw = iw * scale; + const dh = ih * scale; + const cx = sx + drawW + drawW * 0.06; + const cy = sy + drawH * 0.5; + const life = fxUntil != null ? Math.max(0, Math.min(1, (fxUntil - nowT) / STACK_TOWER_PERFECT_X2_MS)) : 1; + const pulse = 0.4 + 0.48 * Math.sin((1 - life) * Math.PI); + if (lightImg && lightImg.complete && lightImg.naturalWidth) { + const lp = 1.06; + const lw = dw * lp; + const lh = dh * lp; + const lnx = lightImg.naturalWidth; + const lny = lightImg.naturalHeight; + ctx.save(); + ctx.globalAlpha = Math.min(0.9, pulse + 0.12); + ctx.globalCompositeOperation = 'screen'; + try { + ctx.drawImage(lightImg, 0, 0, lnx, lny, cx - lw * 0.5, cy - lh * 0.5, lw, lh); + } catch (e) { /* ignore */ } + ctx.restore(); + } + ctx.save(); + ctx.globalAlpha = 0.98; + ctx.globalCompositeOperation = 'source-over'; + try { + ctx.drawImage(x2Img, 0, 0, iw, ih, cx - dw * 0.48, cy - dh * 0.5, dw, dh); + } catch (e) { /* ignore */ } + ctx.restore(); + } + + function drawStackTowerScorePopupsPlay(ctx, worldToScreen, zoom, m, layerWorldH, floorY) { + if (!m.scorePopups || !m.scorePopups.length) return; + const now = performance.now(); + const dur = STACK_TOWER_SCORE_POPUP_MS; + const imgs = ensureStackTowerScorePopupImagesPlay(); + m.scorePopups = m.scorePopups.filter((p) => p.until > now); + const hidPop = getStackTowerVisualHiddenLayerCountPlay(); + for (let pi = 0; pi < m.scorePopups.length; pi++) { + const p = m.scorePopups[pi]; + const li = Math.floor(Number(p.layerIndex)) || 0; + if (li < 0 || li >= m.layers.length) continue; + if (li < hidPop) continue; + const L = m.layers[li]; + const lw = (L.w != null ? L.w : m.initialWidthTiles) * tileSize * getStackTowerBlockWorldScalePlay(); + const cxW = L.cx * tileSize; + const yb = floorY - (li + 1) * layerWorldH; + const xl = cxW - lw / 2; + const [sx, sy] = worldToScreen(xl, yb); + const drawW = lw * zoom; + const drawH = layerWorldH * zoom; + const t0 = p.until - dur; + const t = Math.max(0, Math.min(1, (now - t0) / dur)); + const alpha = 0.2 + 0.78 * Math.sin(Math.PI * t); + const rise = Math.sin((t * Math.PI) / 2) * 20 * zoom; + const im = p.kind === '20' ? imgs.p20 : imgs.p10; + const targetW = Math.min(drawW * 0.92, Math.max(52, 64 * zoom)); + let iw = targetW; + let ih = targetW * 0.38; + if (im && im.complete && im.naturalWidth > 0 && im.naturalHeight > 0) { + ih = (targetW * im.naturalHeight) / im.naturalWidth; + } + const cx = sx + drawW / 2; + const cy = sy - Math.max(8, 6 * zoom) - rise - ih * 0.5; + ctx.save(); + ctx.globalAlpha = Math.min(0.98, Math.max(0.08, alpha)); + if (im && im.complete && im.naturalWidth > 0) { + try { + ctx.drawImage(im, 0, 0, im.naturalWidth, im.naturalHeight, cx - iw / 2, cy - ih / 2, iw, ih); + } catch (e) { /* ignore */ } + } else { + ctx.font = 'bold ' + Math.round(Math.max(12, 15 * zoom)) + 'px ui-monospace, monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = p.kind === '20' ? '#7af8ff' : '#b4f9cd'; + ctx.fillText(p.kind === '20' ? '+20' : '+10', cx, cy); + } + ctx.restore(); + } + } + + function ensureStackTowerHeartMinusImgPlay() { + if (!stackTowerHeartMinusImg) { + stackTowerHeartMinusImg = new Image(); + stackTowerHeartMinusImg.decoding = 'async'; + stackTowerHeartMinusImg.src = stackTowerAssetUrl('heart-1.png'); + } + const im = stackTowerHeartMinusImg; + return im && im.complete && im.naturalWidth > 0 ? im : null; + } + + function ensureStackTowerLifeHudImagesPlay() { + if (!stackTowerLifeBarImg) { + stackTowerLifeBarImg = new Image(); + stackTowerLifeBarImg.decoding = 'async'; + stackTowerLifeBarImg.src = stackTowerAssetUrl('life-bar.png'); + } + if (!stackTowerLifeHitImg) { + stackTowerLifeHitImg = new Image(); + stackTowerLifeHitImg.decoding = 'async'; + stackTowerLifeHitImg.src = stackTowerAssetUrl('life-hit.png'); + } + if (!stackTowerLifeFullImg) { + stackTowerLifeFullImg = new Image(); + stackTowerLifeFullImg.decoding = 'async'; + stackTowerLifeFullImg.src = stackTowerAssetUrl('life-full.png'); + } + } + + /** + * สัดส่วนสูงของ life-bar.png ที่เป็นข้อความ "SYSTEM INTEGRITY" (ที่เหลือ = แถวหัวใจแยกวาดเอง ไม่ทับขอบ `[` `]` ใน PNG) + */ + const STACK_TOWER_LIFE_BAR_TITLE_FRAC = 0.38; + + /** + * วาดหัวข้อจาก life-bar.png (ครอปบน) + แถวหัวใจ life-full / life-hit กึ่งกลาง — คืนความสูงที่ใช้จริง + */ + function drawStackTowerLifeIntegrityBarPlay(ctx, destX, destY, maxW, maxH, lifeSlots, livesRem) { + ensureStackTowerLifeHudImagesPlay(); + const bar = stackTowerLifeBarImg; + const fullIm = (stackTowerLifeFullImg && stackTowerLifeFullImg.complete && stackTowerLifeFullImg.naturalWidth > 0) + ? stackTowerLifeFullImg + : ensureStackTowerHeartMinusImgPlay(); + const hitIm = stackTowerLifeHitImg && stackTowerLifeHitImg.complete && stackTowerLifeHitImg.naturalWidth > 0 + ? stackTowerLifeHitImg + : null; + const slots = Math.max(1, Math.min(20, Math.floor(lifeSlots) || 1)); + const lives = Math.max(0, Math.min(slots, Math.floor(livesRem) || 0)); + + if (!bar || !bar.complete || !bar.naturalWidth || maxW <= 4 || maxH <= 4) { + ctx.save(); + ctx.fillStyle = '#bb9af7'; + ctx.font = '600 9px ui-sans-serif, system-ui, sans-serif'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + let heartStr = ''; + for (let i = 0; i < slots; i++) heartStr += i < lives ? '♥ ' : '♡ '; + ctx.fillText('SYSTEM INTEGRITY ' + heartStr.trim(), destX, destY + maxH * 0.5); + ctx.restore(); + return Math.min(maxH, 28); + } + + const bw0 = bar.naturalWidth; + const bh0 = bar.naturalHeight; + const titleSrcH = Math.max(2, Math.floor(bh0 * STACK_TOWER_LIFE_BAR_TITLE_FRAC)); + const scale = Math.min(maxW / bw0, 1.45); + const bw = bw0 * scale; + const titleDh = titleSrcH * scale; + const gap = 3; + const bx = destX; + const by0 = destY + Math.max(0, (maxH - titleDh - gap - Math.min(40, maxH * 0.45)) * 0.35); + const heartsTop = by0 + titleDh + gap; + const roomHearts = Math.max(10, destY + maxH - heartsTop - 2); + const heartsH = Math.max(18, Math.min(roomHearts, 40, Math.floor(maxH * 0.5))); + + ctx.save(); + try { + ctx.drawImage(bar, 0, 0, bw0, titleSrcH, bx, by0, bw, titleDh); + } catch (e) { /* ignore */ } + + const cy = heartsTop + heartsH * 0.5; + const bracketW = Math.min(14, Math.max(9, bw * 0.07)); + ctx.fillStyle = 'rgba(94, 239, 255, 0.88)'; + ctx.font = '600 ' + Math.round(Math.min(17, heartsH * 0.55)) + 'px ui-monospace, "Cascadia Mono", Consolas, monospace'; + ctx.textBaseline = 'middle'; + ctx.textAlign = 'left'; + ctx.shadowColor = 'rgba(0, 234, 255, 0.45)'; + ctx.shadowBlur = 4; + ctx.fillText('[', bx + 1, cy); + ctx.textAlign = 'right'; + ctx.fillText(']', bx + bw - 1, cy); + ctx.shadowBlur = 0; + + const ix = bx + bracketW + 4; + const iwInner = Math.max(24, bw - (bracketW + 4) * 2); + const slotW = iwInner / slots; + const iconMax = Math.min(heartsH * 0.78, slotW * 0.72, 34); + + for (let i = 0; i < slots; i++) { + const cx = ix + slotW * (i + 0.5); + const useFull = i < lives; + const im = useFull ? fullIm : hitIm; + if (im && im.complete && im.naturalWidth > 0 && im.naturalHeight > 0) { + const nw = im.naturalWidth; + const nh = im.naturalHeight; + let ih = iconMax; + let iw = (ih * nw) / nh; + const capW = slotW * 0.82; + const capH = heartsH * 0.88; + if (iw > capW) { + iw = capW; + ih = (iw * nh) / nw; + } + if (ih > capH) { + ih = capH; + iw = (ih * nw) / nh; + } + if (iw < 6 && nh > 0) { + iw = 6; + ih = (iw * nh) / nw; + if (ih > capH) { + ih = capH; + iw = (ih * nw) / nh; + } + } + try { + ctx.drawImage(im, 0, 0, nw, nh, cx - iw / 2, cy - ih / 2, iw, ih); + } catch (e2) { /* ignore */ } + } else { + ctx.fillStyle = useFull ? '#9ece6a' : '#565f89'; + ctx.font = 'bold ' + Math.round(Math.max(10, iconMax * 0.72)) + 'px sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(useFull ? '♥' : '♡', cx, cy); + } + } + ctx.restore(); + const usedH = heartsTop + heartsH - by0 + 2; + return Math.min(maxH, Math.max(titleDh, usedH)); + } + + function triggerStackTowerHeartMinusFxPlay() { + if (!isStackTowerMissionHudActivePlay()) return; + stackTowerHeartMinusFx = { until: performance.now() + STACK_TOWER_HEART_MINUS_FX_MS }; + } + + function drawStackTowerHeartMinusFxPlay(ctx, worldToScreen, zoom, m, layerWorldH, floorY, nLay) { + if (!stackTowerHeartMinusFx) return; + const nt = performance.now(); + if (nt >= stackTowerHeartMinusFx.until) { + stackTowerHeartMinusFx = null; + return; + } + const dur = STACK_TOWER_HEART_MINUS_FX_MS; + const t0 = stackTowerHeartMinusFx.until - dur; + const u = Math.max(0, Math.min(1, (nt - t0) / dur)); + const alpha = 1 - u * u; + const worldCx = m.topCenterX * tileSize; + const stackTopY = floorY - nLay * layerWorldH; + const worldCy = stackTopY - tileSize * 1.55 - u * tileSize * 0.5; + const [sx, sy] = worldToScreen(worldCx, worldCy); + const im = ensureStackTowerHeartMinusImgPlay(); + const maxW = Math.max(72, Math.min(240, 6.8 * tileSize * zoom)); + let dw = maxW; + let dh = maxW * 0.52; + if (im && im.naturalWidth > 0 && im.naturalHeight > 0) { + dh = (maxW * im.naturalHeight) / im.naturalWidth; + } + ctx.save(); + ctx.globalAlpha = Math.max(0, Math.min(0.98, alpha)); + if (im) { + ctx.drawImage(im, sx - dw / 2, sy - dh / 2, dw, dh); + } else { + ctx.fillStyle = '#f7768e'; + ctx.font = 'bold ' + Math.round(Math.max(14, 18 * zoom)) + 'px ui-monospace, monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('♥ −1', sx, sy); + } + ctx.restore(); + } + + /** Glow สี่เหลี่ยมสีเหลือง — ขอบเขตเท่ากล่องบล็อก (drawW×drawH) ไม่ล้ำออกนอกสไปรต์ */ + function drawStackTowerPerfectYellowGlowBehindBlockPlay(ctx, sx, sy, drawW, drawH, nowT, untilT) { + if (drawW <= 0 || drawH <= 0) return; + const life = Math.max(0, Math.min(1, (untilT - nowT) / STACK_TOWER_PERFECT_GLOW_MS)); + const pulse = 0.38 + 0.48 * Math.sin((1 - life) * Math.PI); + ctx.save(); + ctx.beginPath(); + ctx.rect(sx, sy, drawW, drawH); + ctx.clip(); + ctx.globalCompositeOperation = 'source-over'; + ctx.shadowColor = 'rgba(255, 210, 40, 0.9)'; + ctx.shadowBlur = Math.max(3, Math.min(10, 4 + pulse * 5)); + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + ctx.globalAlpha = Math.min(0.85, 0.4 + pulse * 0.34); + ctx.fillStyle = 'rgba(255, 228, 72, 0.52)'; + ctx.fillRect(sx, sy, drawW, drawH); + ctx.shadowBlur = 0; + ctx.shadowColor = 'transparent'; + ctx.globalAlpha = Math.min(0.72, 0.26 + pulse * 0.32); + ctx.strokeStyle = 'rgba(255, 250, 200, 0.78)'; + const sw = Math.max(1.2, Math.min(2.8, drawW * 0.022)); + ctx.lineWidth = sw; + ctx.strokeRect(sx + sw * 0.5, sy + sw * 0.5, drawW - sw, drawH - sw); + ctx.restore(); + } + + /** + * เชือกจากจุดหนีบบน (จอ) ลงจุดยึดบล็อก — รูป sling แนวตั้ง (แกน Y = สาย) ยืดตามความยาว + */ + function drawStackSlingSegmentPlay(ctx, sx0, sy0, sx1, sy1, zoom) { + const dx = sx1 - sx0; + const dy = sy1 - sy0; + const len = Math.hypot(dx, dy); + if (len < 1.5) return; + const sling = ensureStackTowerSlingImagePlay(); + if (sling) { + const nw = sling.naturalWidth; + const nh = sling.naturalHeight; + const ropeW = Math.max(3, Math.min(40, nw * Math.min(1.1, zoom * 0.11) + 2)); + const ang = Math.atan2(dy, dx); + ctx.save(); + ctx.translate(sx0, sy0); + ctx.rotate(ang - Math.PI / 2); + ctx.globalAlpha = 0.96; + try { + ctx.drawImage(sling, -ropeW / 2, 0, ropeW, len); + } catch (e) { /* ignore */ } + ctx.globalAlpha = 1; + ctx.restore(); + } else { + ctx.strokeStyle = 'rgba(90, 88, 86, 0.82)'; + ctx.lineWidth = Math.max(2.2, zoom * 1.4); + ctx.beginPath(); + ctx.moveTo(sx0, sy0); + ctx.lineTo(sx1, sy1); + ctx.stroke(); + ctx.lineWidth = 1; + } + } + + const GCHOWTO_STACK_TOWER_CLASS = 'gauntlet-crown-howto-overlay--stack-tower'; + + function setGauntletCrownHowtoStackTowerArtMaskPlay(on) { + const hov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!hov) return; + hov.classList.toggle(GCHOWTO_STACK_TOWER_CLASS, !!on); + } + + function hideStackTowerHowtoRulesDom() { + /* กล่องกติกา Stack Tower ถูกถอนออกจาก DOM — เก็บชื่อฟังก์ชันเพื่อไม่ให้จุดเรียกพัง */ + } + + function teardownStackTowerMissionUiPlay() { + stackTowerMissionPhase = null; + stackTowerMissionDeferredPhase = null; + stackTowerSessionStartAt = 0; + stackTowerMissionEndedOnce = false; + stackTowerPost50AnimStartMs = null; + hideStackTowerHowtoRulesDom(); + setGauntletCrownHowtoStackTowerArtMaskPlay(false); + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + hideStackTowerResultFlashDomOnlyPlay(); + } + + function hideStackTowerResultFlashDomOnlyPlay() { + if (stackTowerResultFlashTimer) { + clearTimeout(stackTowerResultFlashTimer); + stackTowerResultFlashTimer = null; + } + const ov = document.getElementById('stack-tower-result-flash'); + const img = document.getElementById('stack-tower-result-flash-img'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + if (img) { + img.onerror = null; + img.removeAttribute('src'); + } + } + + /** โชว์รูปผล Tower จาก TowerBlock 5 วิ แล้วเปิด GCM สรุปเดิม */ + function beginStackTowerResultFlashThenGcm(mission) { + if (!mission || mission.uiSkin !== 'stack_tower') { + showGauntletCrownMissionOverlay(mission); + return; + } + hideStackTowerResultFlashDomOnlyPlay(); + const ov = document.getElementById('stack-tower-result-flash'); + const imgEl = document.getElementById('stack-tower-result-flash-img'); + if (!ov || !imgEl) { + showGauntletCrownMissionOverlay(mission); + return; + } + const outc = String(mission.stackTowerOutcome || 'time_up'); + let file = 'result-timeup.png'; + if (outc === 'decrypt_complete') file = 'result-complete.png'; + else if (outc === 'misses_exhausted') file = 'result-gameover.png'; + const url = stackTowerAssetUrl(file); + const finishToGcm = function () { + hideStackTowerResultFlashDomOnlyPlay(); + showGauntletCrownMissionOverlay(mission); + }; + imgEl.onerror = function () { + imgEl.onerror = null; + finishToGcm(); + }; + imgEl.src = url; + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + stackTowerResultFlashTimer = setTimeout(function () { + stackTowerResultFlashTimer = null; + finishToGcm(); + }, STACK_TOWER_RESULT_FLASH_MS); + } + + /** Jumper mnptfts2: เกรด F = ภาพ gameover · อื่น = complete (ก่อนแผงสรุปผล) */ + function jumpSurviveMissionOutcomeImageFile(mission) { + const g = mission && String(mission.grade || '').toUpperCase(); + if (g === 'F') return 'result-gameover.png'; + return 'result-complete.png'; + } + + /** + * Jumper ภารกิจ: ใช้ #stack-tower-result-flash เหมือน Tower — time_up = timeup แล้ว outcome; + * all_dead = ข้าม timeup แล้ว outcome อย่างเดียว แล้วค่อย GCM + */ + function beginJumpSurviveMissionResultFlashSequenceThenGcm(mission, endKind) { + if (!mission || mission.uiSkin !== 'jumper') { + showGauntletCrownMissionOverlay(mission); + return; + } + hideStackTowerResultFlashDomOnlyPlay(); + const ov = document.getElementById('stack-tower-result-flash'); + const imgEl = document.getElementById('stack-tower-result-flash-img'); + if (!ov || !imgEl) { + showGauntletCrownMissionOverlay(mission); + return; + } + const outcomeFile = jumpSurviveMissionOutcomeImageFile(mission); + const finishToGcm = function () { + hideStackTowerResultFlashDomOnlyPlay(); + showGauntletCrownMissionOverlay(mission); + }; + const showOneFlash = function (file, onDone) { + imgEl.onerror = function () { + imgEl.onerror = null; + onDone(); + }; + imgEl.src = jumperAssetUrl(file); + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + stackTowerResultFlashTimer = setTimeout(function () { + stackTowerResultFlashTimer = null; + onDone(); + }, STACK_TOWER_RESULT_FLASH_MS); + }; + if (endKind === 'time_up') { + showOneFlash('result-timeup.png', function () { + hideStackTowerResultFlashDomOnlyPlay(); + showOneFlash(outcomeFile, finishToGcm); + }); + } else { + showOneFlash(outcomeFile, finishToGcm); + } + } + + /** + * Space Shooter mnpz6rkp: หมดเวลา = แฟลช result-timeup แล้ว result-gameover แล้ว GCM; + * ทุกคนตาย = แฟลช result-gameover อย่างเดียวแล้ว GCM + */ + function beginSpaceShooterMissionResultFlashSequenceThenGcm(mission, endKind) { + if (!mission || mission.uiSkin !== 'violent_crime') { + showGauntletCrownMissionOverlay(mission); + return; + } + hideStackTowerResultFlashDomOnlyPlay(); + const ov = document.getElementById('stack-tower-result-flash'); + const imgEl = document.getElementById('stack-tower-result-flash-img'); + if (!ov || !imgEl) { + showGauntletCrownMissionOverlay(mission); + return; + } + const finishToGcm = function () { + hideStackTowerResultFlashDomOnlyPlay(); + showGauntletCrownMissionOverlay(mission); + }; + const showOneFlash = function (file, onDone) { + imgEl.onerror = function () { + imgEl.onerror = null; + onDone(); + }; + imgEl.src = violentCrimeAssetUrl(file); + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + stackTowerResultFlashTimer = setTimeout(function () { + stackTowerResultFlashTimer = null; + onDone(); + }, STACK_TOWER_RESULT_FLASH_MS); + }; + if (endKind === 'time_up') { + showOneFlash('result-timeup.png', function () { + hideStackTowerResultFlashDomOnlyPlay(); + showOneFlash('result-gameover.png', finishToGcm); + }); + } else { + showOneFlash('result-gameover.png', finishToGcm); + } + } + + /** + * Mega Virus (balloon_boss shell): แฟลชรูปเดียวแล้วค่อย GCM + * — แพ้ (ทุกคนตาย / หมดเวลา) = result-gameover.png (TowerBlock เหมือนภารกิจอื่น — รูป2 flow) + * — ชนะ = end-victory-2.png ใน MegaVirus + */ + function beginBalloonBossMegaVirusResultFlashSequenceThenGcm(mission, endReason) { + if (!mission || mission.uiSkin !== 'mega_virus') { + showGauntletCrownMissionOverlay(mission); + return; + } + hideStackTowerResultFlashDomOnlyPlay(); + const ov = document.getElementById('stack-tower-result-flash'); + const imgEl = document.getElementById('stack-tower-result-flash-img'); + if (!ov || !imgEl) { + showGauntletCrownMissionOverlay(mission); + return; + } + const lose = endReason === 'all_dead' || endReason === 'time'; + const flashSrc = lose ? stackTowerAssetUrl('result-gameover.png') : megaVirusAssetUrl('end-victory-2.png'); + const finishToGcm = function () { + hideStackTowerResultFlashDomOnlyPlay(); + showGauntletCrownMissionOverlay(mission); + }; + imgEl.onerror = function () { + imgEl.onerror = null; + finishToGcm(); + }; + imgEl.src = flashSrc; + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + stackTowerResultFlashTimer = setTimeout(function () { + stackTowerResultFlashTimer = null; + finishToGcm(); + }, STACK_TOWER_RESULT_FLASH_MS); + } + + function stackTowerMissionTimeLimitSecPlay() { + const t = Number(playStackTowerMissionTimeSec); + if (Number.isFinite(t) && t > 0) return Math.max(10, Math.min(7200, Math.floor(t))); + return 90; + } + + function stackTowerProgressBlocksPlay() { + const n = Number(playStackTowerProgressBlocks); + if (Number.isFinite(n) && n >= 1) return Math.max(1, Math.min(500, Math.floor(n))); + return 50; + } + + function stackTowerDecryptPctPlay() { + if (!stackMini) return 0; + if (isStackTowerMissionUiMapPlay()) { + const p = Number(stackMini.progressPct); + const x = Math.min(100, Math.max(0, Number.isFinite(p) ? p : 0)); + if (x >= 99.999) return 100; + return Math.floor(x); + } + const teamScore = stackMini.score || 0; + return Math.min(100, Math.floor((stackMini.layers.length || 0) * 11 + Math.min(40, teamScore * 3))); + } + + function stackTowerMissionElapsedSecPlay() { + if (stackTowerSessionStartAt <= 0) return 0; + return (performance.now() - stackTowerSessionStartAt) / 1000; + } + + function applyStackTowerMissionPanelImages() { + const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg'); + if (howtoBg) { + howtoBg.src = stackTowerAssetUrl('popup-Howto.png'); + howtoBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-Howto.png'; + }; + } + const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg'); + if (resBg) { + resBg.src = stackTowerAssetUrl('popup-result.png'); + resBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-result.png'; + }; + } + ensureStackTowerSlingImagePlay(); + } + + function showStackTowerMissionHowtoOverlay() { + if (!isStackTowerMissionUiMapPlay()) return; + hideConflictingOverlaysForGauntletCrown(); + applyStackTowerMissionPanelImages(); + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + stackTowerMissionPhase = 'howto'; + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + setGauntletCrownHowtoStackTowerArtMaskPlay(true); + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) st.classList.remove('gch-status--jumper'); + const btn = document.getElementById('btn-gch-ready'); + const humans = quizCarryPregameHumanIds(); + const totPlayers = Math.max(1, quizCarryPregameTotalPlayers()); + const soleParticipant = totPlayers === 1; + if (soleParticipant) { + if (st) { + st.textContent = ''; + st.classList.add('is-hidden'); + } + if (btn) { + const canGo = isMePlayHost() || humans.length === 1; + btn.classList.remove('is-start-phase'); + btn.classList.toggle('is-read-only', !canGo); + btn.disabled = !canGo; + btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host'; + btn.setAttribute('aria-pressed', 'false'); + } + } else { + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request'); + gauntletCrownSyncGuestReadyIfNeeded(); + updateStackTowerMissionHowtoHud(); + } + } + + /** Stack Tower mnn93hpi — Ready Status / ปุ่ม แบบ quiz mission (mng8a80o) */ + function updateStackTowerMissionHowtoHud() { + if (!isStackTowerMissionUiMapPlay() || stackTowerMissionPhase !== 'howto') return; + const st = document.getElementById('gauntlet-crown-howto-status'); + const btn = document.getElementById('btn-gch-ready'); + if (!st || !btn) return; + const humans = quizCarryPregameHumanIds(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + const num = gauntletCrownPregameReadyNumerator(); + st.classList.remove('is-hidden'); + st.textContent = 'Ready Status : ' + num + '/' + tot; + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + btn.classList.toggle('is-start-phase', humansReady); + btn.classList.toggle('is-read-only', !isMePlayHost()); + btn.disabled = !isMePlayHost(); + btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false')); + btn.title = isMePlayHost() + ? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + + /** Jump Survival mnptfts2 — Ready Status / ปุ่ม เดียวกับ Stack Tower */ + function updateJumpSurviveMissionHowtoHud() { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveMissionPhase !== 'howto') return; + const st = document.getElementById('gauntlet-crown-howto-status'); + const btn = document.getElementById('btn-gch-ready'); + if (!st || !btn) return; + const humans = quizCarryPregameHumanIds(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + const num = gauntletCrownPregameReadyNumerator(); + st.classList.remove('is-hidden'); + st.textContent = 'Ready Status : ' + num + '/' + tot; + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + btn.classList.toggle('is-start-phase', humansReady); + btn.classList.toggle('is-read-only', !isMePlayHost()); + btn.disabled = !isMePlayHost(); + btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false')); + btn.title = isMePlayHost() + ? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + + /** Space Shooter mnpz6rkp — Ready Status / ปุ่ม เดียวกับ Jumper */ + function updateSpaceShooterMissionHowtoHud() { + if (!isSpaceShooterMissionUiMapPlay() || spaceShooterMissionPhase !== 'howto') return; + const st = document.getElementById('gauntlet-crown-howto-status'); + const btn = document.getElementById('btn-gch-ready'); + if (!st || !btn) return; + const humans = quizCarryPregameHumanIds(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + const num = gauntletCrownPregameReadyNumerator(); + st.classList.remove('is-hidden'); + st.textContent = 'Ready Status : ' + num + '/' + tot; + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + btn.classList.toggle('is-start-phase', humansReady); + btn.classList.toggle('is-read-only', !isMePlayHost()); + btn.disabled = !isMePlayHost(); + btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false')); + btn.title = isMePlayHost() + ? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + + function beginStackTowerMissionCountdownThenRun() { + if (!isStackTowerMissionUiMapPlay()) return; + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + stackTowerMissionPhase = 'countdown'; + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) { + howto.classList.add('is-hidden'); + howto.classList.remove(GCHOWTO_STACK_TOWER_CLASS); + } + gauntletCrownHowtoVisible = false; + hideStackTowerHowtoRulesDom(); + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) { + st.classList.add('is-hidden'); + st.classList.remove('gch-status--jumper'); + } + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + const runFinish = function () { + if (cd) cd.classList.add('is-hidden'); + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + stackTowerMissionPhase = 'live'; + stackTowerMissionEndedOnce = false; + stackTowerSessionStartAt = performance.now(); + lastStackTickMs = performance.now(); + resetStackMinigameState(); + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + setCountdown321QuestionAssetGraphic(numEl, n); + const step = function () { + n--; + if (n > 0) { + setCountdown321QuestionAssetGraphic(numEl, n); + stackTowerMissionCountdownTimer = setTimeout(step, 1000); + } else { + stackTowerMissionCountdownTimer = null; + runFinish(); + } + }; + stackTowerMissionCountdownTimer = setTimeout(step, 1000); + } + + function stackTowerMissionBuildPayload(endOpts) { + const outcome = endOpts && endOpts.outcome ? String(endOpts.outcome) : 'decrypt_complete'; + const rows = []; + const teamPts = Math.max(0, Number(stackMini && stackMini.score) || 0); + function pushEnt(id, nickname, characterId, score) { + rows.push({ + id: id, + nickname: (nickname && String(nickname).trim()) ? String(nickname).trim() : String(id), + characterId: characterId ?? null, + baseScore: Math.max(0, Number(score) || 0), + eliminated: false, + hadQuizWrong: false, + rankBonus: 0, + finalScore: Math.max(0, Number(score) || 0), + }); + } + if (playBotsEnabled() && stackPreviewTurnOrder && stackPreviewTurnOrder.length === STACK_PREVIEW_TURN_COUNT) { + if (myId != null) { + pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), me.stackPreviewHumanPts || 0); + } + stackPreviewTurnOrder.forEach(function (entry) { + if (!entry || entry.kind !== 'bot' || !entry.botId) return; + const o = others.get(entry.botId); + pushEnt(entry.botId, o ? o.nickname : entry.botId, o ? o.characterId : null, o ? (o.stackBotScore || 0) : 0); + }); + } else { + if (myId != null) { + pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), teamPts); + } + others.forEach(function (o, id) { + if (!o) return; + if (playBotsEnabled() && isPreviewBotId(id)) return; + pushEnt(id, o.nickname, o.characterId, teamPts); + }); + } + const seen = new Set(); + const uniq = []; + rows.forEach(function (r) { + const sid = String(r.id); + if (seen.has(sid)) return; + seen.add(sid); + uniq.push(r); + }); + uniq.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = uniq.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: false, + hadQuizWrong: false, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: row.baseScore, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (totalSum <= 0 && uniq.length) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'stack_tower', + survivorCount: uniq.length, + participantCount: uniq.length, + stackTowerOutcome: outcome, + }; + } + + function stackTowerMissionMergePreviewBotsMission(mission) { + if (!mission || mission.uiSkin !== 'stack_tower' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission; + const seen = new Set(); + (mission.ranked || []).forEach(function (r) { + if (r && r.id != null) seen.add(String(r.id)); + }); + const baseRows = (mission.ranked || []).map(function (r) { + return { + id: r.id, + nickname: r.nickname, + characterId: r.characterId, + baseScore: Math.max(0, Number(r.baseScore) || 0), + eliminated: false, + hadQuizWrong: false, + rankBonus: 0, + finalScore: Math.max(0, Number(r.finalScore != null ? r.finalScore : r.baseScore) || 0), + }; + }); + others.forEach(function (o, id) { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: Math.max(0, Number(o.stackBotScore) || 0), + eliminated: false, + hadQuizWrong: false, + rankBonus: 0, + finalScore: Math.max(0, Number(o.stackBotScore) || 0), + }); + }); + baseRows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: false, + hadQuizWrong: false, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n2 = ranked.length || 1; + const averageScore = Math.floor(totalSum / n2); + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (totalSum <= 0 && baseRows.length) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'stack_tower', + survivorCount: baseRows.length, + participantCount: baseRows.length, + stackTowerOutcome: mission.stackTowerOutcome, + }; + } + + function stackTowerMissionMaybeEndPlay() { + if (!isStackTowerMissionUiMapPlay()) return; + if (stackTowerMissionPhase !== 'live') return; + if (stackTowerMissionEndedOnce) return; + const pct = stackTowerDecryptPctPlay(); + const elapsed = stackTowerMissionElapsedSecPlay(); + const timeLim = stackTowerMissionTimeLimitSecPlay(); + const missesOut = stackMini && stackMini.teamMissesLeft != null && stackMini.teamMissesLeft <= 0; + const timeOut = elapsed >= timeLim; + if (pct < 100 && !timeOut && !missesOut) return; + stackTowerMissionEndedOnce = true; + stackTowerMissionPhase = 'ended'; + applyStackTowerMissionPanelImages(); + let outcome = 'time_up'; + if (pct >= 100) outcome = 'decrypt_complete'; + else if (missesOut) outcome = 'misses_exhausted'; + beginStackTowerResultFlashThenGcm(stackTowerMissionBuildPayload({ outcome: outcome })); + } + + function isJumpSurviveMissionUiMapPlay() { + return isJumpSurvive() && currentPlayMapId() === JUMP_SURVIVE_MISSION_MAP_ID; + } + + function isJumpSurviveMissionPregameBlockingPlay() { + if (!isJumpSurviveMissionUiMapPlay()) return false; + return jumpSurviveMissionPhase != null && jumpSurviveMissionPhase !== 'live' && jumpSurviveMissionPhase !== 'ended'; + } + + function jumperAssetUrl(file) { + return BASE + '/img/Jumper/' + file; + } + + function hideConflictingOverlaysForJumpSurviveMission() { + hideConflictingOverlaysForGauntletCrown(); + } + + function jumpSurviveTimeLimitSecForMission() { + const m = Number(mapData && mapData.jumpSurviveTimeSec); + if (Number.isFinite(m) && m > 0 && m < 7200) return Math.floor(m); + const j = Number(playJumpSurviveMissionTimeSec); + if (Number.isFinite(j) && j > 0 && j < 7200) return Math.floor(j); + return 60; + } + + function jumpSurviveRemainingSecMission() { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveMissionPhase !== 'live' || jumpSurviveSessionStartMs <= 0) return null; + const lim = jumpSurviveTimeLimitSecForMission(); + if (!(lim > 0)) return null; + const elapsed = (performance.now() - jumpSurviveSessionStartMs) / 1000; + return Math.max(0, Math.ceil(lim - elapsed)); + } + + function applyJumpSurviveJumperPanelImages() { + const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg'); + if (howtoBg) { + howtoBg.src = jumperAssetUrl('popup-Howto.png'); + howtoBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-Howto.png'; + }; + } + const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg'); + if (resBg) { + resBg.src = jumperAssetUrl('popup-result.png'); + resBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-result.png'; + }; + } + } + + function showJumpSurviveMissionHowtoOverlay() { + if (!isJumpSurviveMissionUiMapPlay()) return; + hideConflictingOverlaysForJumpSurviveMission(); + applyJumpSurviveJumperPanelImages(); + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + jumpSurviveMissionPhase = 'howto'; + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) st.classList.remove('gch-status--jumper'); + const btn = document.getElementById('btn-gch-ready'); + const humans = quizCarryPregameHumanIds(); + const totPlayers = Math.max(1, quizCarryPregameTotalPlayers()); + const soleParticipant = totPlayers === 1; + if (soleParticipant) { + if (st) { + st.textContent = ''; + st.classList.add('is-hidden'); + } + if (btn) { + const canGo = humans.length === 1 || isMePlayHost(); + btn.classList.remove('is-start-phase'); + btn.classList.toggle('is-read-only', !canGo); + btn.disabled = !canGo; + btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host'; + btn.setAttribute('aria-pressed', 'false'); + } + } else { + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request'); + gauntletCrownSyncGuestReadyIfNeeded(); + updateJumpSurviveMissionHowtoHud(); + } + } + + function beginJumpSurviveMissionCountdownThenRun() { + if (!isJumpSurviveMissionUiMapPlay()) return; + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + jumpSurviveMissionPhase = 'countdown'; + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) howto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) { + st.classList.add('is-hidden'); + st.classList.remove('gch-status--jumper'); + } + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + const runFinish = function () { + if (cd) cd.classList.add('is-hidden'); + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + jumpSurviveMissionPhase = 'live'; + jumpSurviveGameEnded = false; + jumpSurviveEliminated = false; + applyJumpSurvivePreviewSpawnLayout(false); + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + setCountdown321QuestionAssetGraphic(numEl, n); + const step = function () { + n--; + if (n > 0) { + setCountdown321QuestionAssetGraphic(numEl, n); + jumperMissionCountdownTimer = setTimeout(step, 1000); + } else { + jumperMissionCountdownTimer = null; + runFinish(); + } + }; + jumperMissionCountdownTimer = setTimeout(step, 1000); + } + + /** Jumper ภารกิจ mnptfts2: เกรดจากจำนวนผู้รอด — 6=A … 3=D; น้อยกว่า 2 = F; พอดี 2 คน = E */ + function jumpSurviveGradeFromSurvivorCount(survivors) { + const s = Math.max(0, Math.floor(Number(survivors) || 0)); + if (s >= 6) return 'A'; + if (s === 5) return 'B'; + if (s === 4) return 'C'; + if (s === 3) return 'D'; + if (s === 2) return 'E'; + return 'F'; + } + + function jumpSurviveRollRewardCardForGrade(grade) { + if (grade === 'F') return null; + const r = Math.random(); + if (grade === 'A' || grade === 'B') { + if (r < 0.35) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' }; + if (r < 0.7) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'C' || grade === 'D') { + if (r < 0.25) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' }; + if (r < 0.45) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'E') { + if (r < 0.12) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (r < 0.25) return { kind: 'bail', th: 'เหรียญประกัน · Bail Coin', en: 'Bail Coin' }; + if (r < 0.45) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + + function jumpSurviveMissionTotalParticipants() { + let n = myId != null ? 1 : 0; + if (others && typeof others.size === 'number') n += others.size; + return n; + } + + function jumpSurviveMissionCountAlive() { + let n = 0; + if (myId != null && !jumpSurviveEliminated) n++; + if (others && typeof others.forEach === 'function') { + others.forEach(function (o) { + if (o && !o.jumpSurviveEliminated) n++; + }); + } + return n; + } + + /** จบทันทีเมื่อไม่มีผู้รอด — เหลือคนสุดท้ายยังเล่นต่อจนหมดเวลา (ไม่จบเร็วแบบ “ชนะขาดลอย”) */ + function jumpSurviveMissionMaybeEarlyFinish() { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) return; + const alive = jumpSurviveMissionCountAlive(); + if (alive === 0) { + endJumpSurviveMissionRound('all_dead'); + } + } + + function jumpSurviveBuildMissionPayload() { + const rows = []; + const pushEnt = function (id, nickname, characterId, eliminated) { + const alive = !eliminated; + const baseScore = alive ? 100 : 0; + rows.push({ + id: id, + nickname: (nickname && String(nickname).trim()) ? String(nickname).trim() : String(id), + characterId: characterId ?? null, + baseScore: baseScore, + eliminated: !!eliminated, + rankBonus: 0, + finalScore: baseScore, + }); + }; + if (myId != null) { + pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), jumpSurviveEliminated); + } + others.forEach(function (o, id) { + if (!o) return; + pushEnt(id, o.nickname, o.characterId, !!o.jumpSurviveEliminated); + }); + rows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + let survivorCount = 0; + for (let si = 0; si < rows.length; si++) { + if (rows[si] && !rows[si].eliminated) survivorCount++; + } + const participantCount = rows.length; + const grade = jumpSurviveGradeFromSurvivorCount(survivorCount); + const top = rows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: row.baseScore, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + const rewardCard = jumpSurviveRollRewardCardForGrade(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'jumper', + survivorCount: survivorCount, + participantCount: participantCount, + }; + } + + function jumpSurviveMergePreviewBotsMission(mission) { + if (!mission || mission.uiSkin !== 'jumper' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission; + const seen = new Set(); + (mission.ranked || []).forEach(function (r) { + if (r && r.id != null) seen.add(String(r.id)); + }); + const baseRows = (mission.ranked || []).map(function (r) { + return { + id: r.id, + nickname: r.nickname, + characterId: r.characterId, + baseScore: Math.max(0, Number(r.baseScore) || 0), + eliminated: !!r.eliminated, + rankBonus: 0, + }; + }); + others.forEach(function (o, id) { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: o.jumpSurviveEliminated ? 0 : 100, + eliminated: !!o.jumpSurviveEliminated, + rankBonus: 0, + }); + }); + baseRows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + let survivorCount = 0; + for (let si = 0; si < baseRows.length; si++) { + if (baseRows[si] && !baseRows[si].eliminated) survivorCount++; + } + const participantCount = baseRows.length; + const grade = jumpSurviveGradeFromSurvivorCount(survivorCount); + const top = baseRows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: jumpSurviveRollRewardCardForGrade(grade), + totalParts: totalParts, + uiSkin: 'jumper', + survivorCount: survivorCount, + participantCount: participantCount, + }; + } + + function endJumpSurviveMissionRound(endKind) { + if (!isJumpSurviveMissionUiMapPlay() || jumpSurviveGameEnded) return; + jumpSurviveGameEnded = true; + jumpSurviveMissionPhase = 'ended'; + applyJumpSurviveJumperPanelImages(); + const mission = jumpSurviveBuildMissionPayload(); + const kind = endKind === 'all_dead' ? 'all_dead' : 'time_up'; + beginJumpSurviveMissionResultFlashSequenceThenGcm(mission, kind); + } + function isSpaceShooter() { return mapData && mapData.gameType === 'space_shooter'; } + + function isSpaceShooterMissionUiMapPlay() { + return isSpaceShooter() && currentPlayMapId() === SPACE_SHOOTER_MISSION_MAP_ID; + } + + function isSpaceShooterMissionPregameBlockingPlay() { + if (!isSpaceShooterMissionUiMapPlay()) return false; + return spaceShooterMissionPhase != null && spaceShooterMissionPhase !== 'live' && spaceShooterMissionPhase !== 'ended'; + } + + function violentCrimeAssetUrl(file) { + return BASE + '/img/ViolentCrime/' + String(file || '').replace(/^\//, ''); + } + + function megaVirusAssetUrl(file) { + return BASE + '/img/MegaVirus/' + String(file || '').replace(/^\//, ''); + } + + function applyMegaVirusMissionPanelImages() { + const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg'); + if (howtoBg) { + howtoBg.src = megaVirusAssetUrl('popup-Howto.png'); + howtoBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-Howto.png'; + }; + } + const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg'); + if (resBg) { + resBg.src = megaVirusAssetUrl('popup-result.png'); + resBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-result.png'; + }; + } + } + + function applySpaceShooterMissionPanelImages() { + const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg'); + if (howtoBg) { + howtoBg.src = violentCrimeAssetUrl('popup-Howto.png'); + howtoBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-Howto.png'; + }; + } + const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg'); + if (resBg) { + resBg.src = violentCrimeAssetUrl('popup-result.png'); + resBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-result.png'; + }; + } + } + + function showSpaceShooterMissionHowtoOverlay() { + if (!isSpaceShooterMissionUiMapPlay()) return; + hideConflictingOverlaysForGauntletCrown(); + applySpaceShooterMissionPanelImages(); + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + spaceShooterMissionPhase = 'howto'; + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) st.classList.remove('gch-status--jumper'); + const btn = document.getElementById('btn-gch-ready'); + const humans = quizCarryPregameHumanIds(); + const totPlayers = Math.max(1, quizCarryPregameTotalPlayers()); + const soleParticipant = totPlayers === 1; + if (soleParticipant) { + if (st) { + st.textContent = ''; + st.classList.add('is-hidden'); + } + if (btn) { + const canGo = humans.length === 1 || isMePlayHost(); + btn.classList.remove('is-start-phase'); + btn.classList.toggle('is-read-only', !canGo); + btn.disabled = !canGo; + btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host'; + btn.setAttribute('aria-pressed', 'false'); + } + } else { + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request'); + gauntletCrownSyncGuestReadyIfNeeded(); + updateSpaceShooterMissionHowtoHud(); + } + spaceShooterResetMissionShipStateForAllPlay(); + } + + function beginSpaceShooterMissionCountdownThenRun() { + if (!isSpaceShooterMissionUiMapPlay()) return; + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + spaceShooterMissionPhase = 'countdown'; + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) howto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) { + st.classList.add('is-hidden'); + st.classList.remove('gch-status--jumper'); + } + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + const runFinish = function () { + if (cd) cd.classList.add('is-hidden'); + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + spaceShooterMissionPhase = 'live'; + spaceShooterGameEnded = false; + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterAsteroidExplosions = []; + spaceShooterPopups = []; + spaceShooterLastTickMs = performance.now(); + spaceShooterSpawnAccMs = 0; + spaceShooterFireCd = 0; + spaceShooterSessionStartMs = performance.now(); + spaceShooterLastMoveEmit = 0; + if (myId != null) me.spaceShooterScore = 0; + others.forEach(function (o) { + if (o) o.spaceShooterScore = 0; + }); + spaceShooterResetMissionShipStateForAllPlay(); + if (mapData) applySpaceShooterSpawnLayoutPlay(); + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + setCountdown321QuestionAssetGraphic(numEl, n); + const step = function () { + n--; + if (n > 0) { + setCountdown321QuestionAssetGraphic(numEl, n); + spaceShooterMissionCountdownTimer = setTimeout(step, 1000); + } else { + spaceShooterMissionCountdownTimer = null; + runFinish(); + } + }; + spaceShooterMissionCountdownTimer = setTimeout(step, 1000); + } + + function spaceShooterBuildMissionPayload() { + const rows = []; + function pushEnt(id, nickname, characterId, score, eliminated) { + rows.push({ + id: id, + nickname: (nickname && String(nickname).trim()) ? String(nickname).trim() : String(id), + characterId: characterId ?? null, + baseScore: Math.max(0, Number(score) || 0), + eliminated: !!eliminated, + rankBonus: 0, + finalScore: Math.max(0, Number(score) || 0), + }); + } + if (myId != null) { + pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), me.spaceShooterScore, !!me.spaceShooterEliminated); + } + others.forEach(function (o, id) { + if (!o) return; + pushEnt(id, o.nickname, o.characterId, o.spaceShooterScore, !!o.spaceShooterEliminated); + }); + rows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = rows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: row.baseScore, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + const survivorCount = rows.filter(function (r) { return !r.eliminated; }).length; + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (survivorCount <= 0) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'violent_crime', + survivorCount: survivorCount, + participantCount: rows.length, + }; + } + + function spaceShooterMergePreviewBotsMission(mission) { + if (!mission || mission.uiSkin !== 'violent_crime' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission; + const seen = new Set(); + (mission.ranked || []).forEach(function (r) { + if (r && r.id != null) seen.add(String(r.id)); + }); + const baseRows = (mission.ranked || []).map(function (r) { + return { + id: r.id, + nickname: r.nickname, + characterId: r.characterId, + baseScore: Math.max(0, Number(r.baseScore) || 0), + eliminated: !!r.eliminated, + rankBonus: 0, + finalScore: Math.max(0, Number(r.finalScore != null ? r.finalScore : r.baseScore) || 0), + }; + }); + others.forEach(function (o, id) { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: Math.max(0, Number(o.spaceShooterScore) || 0), + eliminated: !!o.spaceShooterEliminated, + rankBonus: 0, + finalScore: Math.max(0, Number(o.spaceShooterScore) || 0), + }); + }); + baseRows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n2 = ranked.length || 1; + const averageScore = Math.floor(totalSum / n2); + const survivorCount = baseRows.filter(function (r) { return !r.eliminated; }).length; + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (survivorCount <= 0) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'violent_crime', + survivorCount: survivorCount, + participantCount: baseRows.length, + }; + } + + function balloonBossMergePreviewBotsMission(mission) { + if (!mission || mission.uiSkin !== 'mega_virus' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission; + const seen = new Set(); + (mission.ranked || []).forEach(function (r) { + if (r && r.id != null) seen.add(String(r.id)); + }); + const baseRows = (mission.ranked || []).map(function (r) { + return { + id: r.id, + nickname: r.nickname, + characterId: r.characterId, + baseScore: Math.max(0, Number(r.baseScore) || 0), + eliminated: !!r.eliminated, + rankBonus: 0, + finalScore: Math.max(0, Number(r.finalScore != null ? r.finalScore : r.baseScore) || 0), + }; + }); + others.forEach(function (o, id) { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: Math.max(0, Number(o.balloonBossScore) || 0), + eliminated: !!o.balloonBossEliminated, + rankBonus: 0, + finalScore: Math.max(0, Number(o.balloonBossScore) || 0), + }); + }); + baseRows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n2 = ranked.length || 1; + const averageScore = Math.floor(totalSum / n2); + const maxHp = balloonBossMaxHpPlay(); + let bossDmgSum = Math.max(0, me.balloonBossBossDmg | 0); + others.forEach(function (o) { + if (o) bossDmgSum += Math.max(0, o.balloonBossBossDmg | 0); + }); + const progress = Math.min(1, bossDmgSum / Math.max(1, maxHp)); + const score100 = Math.min(100, Math.floor(progress * 100)); + let grade = 'C'; + const endR = mission && mission.balloonBossEndReason; + if (endR === 'victory' || progress >= 1) grade = 'A'; + else if (endR === 'all_dead') grade = 'F'; + else if (score100 >= 50) grade = 'B'; + else if (totalSum <= 0) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'mega_virus', + survivorCount: baseRows.filter(function (r) { return !r.eliminated; }).length, + participantCount: baseRows.length, + balloonBossEndReason: mission.balloonBossEndReason, + }; + } + + function balloonBossBuildMissionPayloadPlay(reason) { + const rows = []; + if (myId != null) { + rows.push({ + id: myId, + nickname: ((me.nickname || nick || 'คุณ').trim() || 'คุณ'), + characterId: me.characterId ?? null, + baseScore: Math.max(0, me.balloonBossScore | 0), + eliminated: !!me.balloonBossEliminated, + }); + } + others.forEach((o, id) => { + rows.push({ + id: id, + nickname: (o && o.nickname) ? String(o.nickname).trim() : id, + characterId: o && o.characterId != null ? o.characterId : null, + baseScore: Math.max(0, o.balloonBossScore | 0), + eliminated: !!(o && o.balloonBossEliminated), + }); + }); + rows.sort((a, b) => b.baseScore - a.baseScore + || String(a.nickname).localeCompare(String(b.nickname), 'th') + || String(a.id).localeCompare(String(b.id))); + const top = rows.slice(0, 5); + const ranked = top.map((row, idx) => { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map((r) => r.baseScore); + const totalSum = totalParts.reduce((s, n) => s + n, 0); + const n2 = ranked.length || 1; + const averageScore = Math.floor(totalSum / n2); + const maxHp = balloonBossMaxHpPlay(); + let bossDmgSum = Math.max(0, me.balloonBossBossDmg | 0); + others.forEach((o) => { + if (o) bossDmgSum += Math.max(0, o.balloonBossBossDmg | 0); + }); + const progress = Math.min(1, bossDmgSum / Math.max(1, maxHp)); + const score100 = Math.min(100, Math.floor(progress * 100)); + let grade = 'C'; + if (reason === 'victory' || progress >= 1) grade = 'A'; + else if (reason === 'all_dead') grade = 'F'; + else if (score100 >= 50) grade = 'B'; + else if (totalSum <= 0) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'mega_virus', + survivorCount: rows.filter((r) => !r.eliminated).length, + participantCount: rows.length, + balloonBossEndReason: reason, + }; + } + + function endSpaceShooterMissionRound(endKind) { + if (!isSpaceShooterMissionUiMapPlay() || spaceShooterGameEnded) return; + const kind = endKind === 'all_dead' ? 'all_dead' : 'time_up'; + spaceShooterGameEnded = true; + spaceShooterMissionPhase = 'ended'; + applySpaceShooterMissionPanelImages(); + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterAsteroidExplosions = []; + spaceShooterSpawnAccMs = 0; + spaceShooterPopups = []; + const mission = spaceShooterBuildMissionPayload(); + mission.spaceShooterEndKind = kind; + beginSpaceShooterMissionResultFlashSequenceThenGcm(mission, kind); + } + + function questionMissionAssetUrl(file) { + return BASE + '/img/QUESTION/' + String(file || '').replace(/^\//, ''); + } + + /** HUD กลางจอ (เวลา / แผ่นคำถาม) — ชื่อไฟล์คู่กับ public/img/QUESTION บนดิสก์ → URL /Game/img/QUESTION */ + function questionMissionHudAssetUrl(file) { + return questionMissionAssetUrl(file); + } + + function isQuizQuestionMissionHudActivePlay() { + return isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'live'; + } + + /** ภารกิจคำถาม mng8a80o ช่วง live: กล้องกลางแมป (world px) — ไม่ตาม me */ + function getQuizQuestionMissionMapCenterWorldPxPlay() { + if (!mapData || !isQuiz()) return null; + const w = mapData.width || 20; + const h = mapData.height || 15; + const ts = tileSize; + const mwPx = w * ts; + const mhPx = h * ts; + return { cx: mwPx * 0.5, cy: mhPx * 0.5, mwPx, mhPx }; + } + + function applyQuizQuestionMissionPanelImages() { + const howtoBg = document.querySelector('#gauntlet-crown-howto-overlay .gch-bg'); + if (howtoBg) { + howtoBg.src = questionMissionAssetUrl('popup-Howto.png'); + howtoBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-Howto.png'; + }; + } + const resBg = document.querySelector('#gauntlet-crown-mission-overlay .gcm-bg'); + if (resBg) { + resBg.src = questionMissionAssetUrl('popup-result.png'); + resBg.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/popup-result.png'; + }; + } + } + + function showQuizQuestionMissionHowtoOverlay() { + if (!isQuizQuestionMissionUiMapPlay()) return; + hideConflictingOverlaysForGauntletCrown(); + applyQuizQuestionMissionPanelImages(); + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + quizQuestionMissionPhase = 'howto'; + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) st.classList.remove('gch-status--jumper'); + const btn = document.getElementById('btn-gch-ready'); + const humans = quizCarryPregameHumanIds(); + const totPlayers = Math.max(1, quizCarryPregameTotalPlayers()); + /** ผู้เล่นจริงในห้องคนเดียว และไม่มีบอท/peer อื่นในนับรวม — เท่านั้นที่ข้าม lobby */ + const soleParticipant = totPlayers === 1; + if (soleParticipant) { + if (st) { + st.textContent = ''; + st.classList.add('is-hidden'); + } + if (btn) { + const canGo = isMePlayHost() || humans.length === 1; + btn.classList.remove('is-start-phase'); + btn.classList.toggle('is-read-only', !canGo); + btn.disabled = !canGo; + btn.title = canGo ? 'READY — เริ่มนับถอยหลัง' : 'รอโฮสต์ · Wait for host'; + btn.setAttribute('aria-pressed', 'false'); + } + } else { + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request'); + gauntletCrownSyncGuestReadyIfNeeded(); + updateQuizQuestionMissionHowtoHud(); + } + } + + function beginQuizQuestionMissionCountdownThenRun() { + if (!isQuizQuestionMissionUiMapPlay()) return; + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + quizQuestionMissionPhase = 'countdown'; + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) howto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + const st = document.getElementById('gauntlet-crown-howto-status'); + if (st) { + st.classList.add('is-hidden'); + st.classList.remove('gch-status--jumper'); + } + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + const runFinish = function () { + if (cd) cd.classList.add('is-hidden'); + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + quizQuestionMissionPhase = 'live'; + playEmbedUserZoomMul = 1; + const qov = document.getElementById('quiz-game-overlay'); + if (qov) { + if (isQuizQuestionMissionUiMapPlay()) qov.classList.add('is-hidden'); + else if (previewMode && editorEmbedReturn) qov.classList.add('is-hidden'); + else qov.classList.remove('is-hidden'); + } + const dp = quizQuestionMissionDeferredPhase; + quizQuestionMissionDeferredPhase = null; + if (dp) applyQuizPhaseFromServer(dp); + if (previewMode && isQuiz()) loadPreviewQuizAndStart(); + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + setCountdown321QuestionAssetGraphic(numEl, n); + const step = function () { + n--; + if (n > 0) { + setCountdown321QuestionAssetGraphic(numEl, n); + quizQuestionMissionCountdownTimer = setTimeout(step, 1000); + } else { + quizQuestionMissionCountdownTimer = null; + runFinish(); + } + }; + quizQuestionMissionCountdownTimer = setTimeout(step, 1000); + } + + function quizQuestionMissionBuildPayload() { + const rows = []; + function pushEnt(id, nickname, characterId, score) { + const sid = String(id); + rows.push({ + id: id, + nickname: (nickname && String(nickname).trim()) ? String(nickname).trim() : String(id), + characterId: characterId ?? null, + baseScore: Math.max(0, Number(score) || 0), + eliminated: false, + hadQuizWrong: !!playQuizEverWrong[sid], + rankBonus: 0, + finalScore: Math.max(0, Number(score) || 0), + }); + } + if (myId != null) { + pushEnt(myId, (me.nickname || nick || 'คุณ').trim() || 'คุณ', me.characterId || getPlayCharacterId(), playLiveQuizScores[myId]); + } + others.forEach(function (o, id) { + if (!o) return; + pushEnt(id, o.nickname, o.characterId, playLiveQuizScores[id]); + }); + rows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = rows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: false, + hadQuizWrong: !!row.hadQuizWrong, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: row.baseScore, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (totalSum <= 0 && rows.length) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'question_mission', + survivorCount: rows.length, + participantCount: rows.length, + }; + } + + function quizQuestionMissionMergePreviewBotsMission(mission) { + if (!mission || mission.uiSkin !== 'question_mission' || !playBotsEnabled() || !others || typeof others.forEach !== 'function') return mission; + const seen = new Set(); + (mission.ranked || []).forEach(function (r) { + if (r && r.id != null) seen.add(String(r.id)); + }); + const baseRows = (mission.ranked || []).map(function (r) { + return { + id: r.id, + nickname: r.nickname, + characterId: r.characterId, + baseScore: Math.max(0, Number(r.baseScore) || 0), + eliminated: false, + hadQuizWrong: !!r.hadQuizWrong, + rankBonus: 0, + finalScore: Math.max(0, Number(r.finalScore != null ? r.finalScore : r.baseScore) || 0), + }; + }); + others.forEach(function (o, id) { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: Math.max(0, Number(playLiveQuizScores[id]) || 0), + eliminated: false, + hadQuizWrong: !!playQuizEverWrong[sid], + rankBonus: 0, + finalScore: Math.max(0, Number(playLiveQuizScores[id]) || 0), + }); + }); + baseRows.sort(function (a, b) { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + return String(a.nickname).localeCompare(String(b.nickname), 'th') || String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map(function (row, idx) { + const pos = idx + 1; + const bs = Math.max(0, Number(row.baseScore) || 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: false, + hadQuizWrong: !!row.hadQuizWrong, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus: 0, + finalScore: bs, + }; + }); + const totalParts = ranked.map(function (r) { return r.baseScore; }); + const totalSum = totalParts.reduce(function (s, n) { return s + n; }, 0); + const n2 = ranked.length || 1; + const averageScore = Math.floor(totalSum / n2); + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (totalSum <= 0 && baseRows.length) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: totalParts, + uiSkin: 'question_mission', + survivorCount: baseRows.length, + participantCount: baseRows.length, + }; + } + + function isBalloonBoss() { return mapData && mapData.gameType === 'balloon_boss'; } + function isQuizCarry() { return mapData && mapData.gameType === 'quiz_carry'; } + /** quiz_carry: จุดกลางแมป (world px) — กล้องไม่ตามตัวละคร; ใช้ร่วม draw + overlay DOM */ + function getQuizCarryMapCameraWorldCenterPxPlay() { + if (!mapData || mapData.gameType !== 'quiz_carry') return null; + const ww = mapData.width || 20, hh = mapData.height || 15; + const ts = tileSize; + return { cx: ww * ts * 0.5, cy: hh * ts * 0.5 }; + } + function isQuizBattle() { return mapData && mapData.gameType === 'quiz_battle'; } + + function quizCarryNeonColorForChoice(choiceIndex) { + const i = Math.max(0, choiceIndex | 0); + const h = (i * 47 + 100) % 360; + return 'hsl(' + h + ', 72%, 62%)'; + } + + function quizCarryMinimapOptionFillCss(ov) { + const th = getEffectiveCarryChoicePlaqueThemeForChoice((Number(ov) | 0) - 1); + if (th.borderMode === 'fixed' && th.fixedBorder) { + const m = /^rgba?\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*(?:,\s*([0-9.]+)\s*)?\)$/i.exec(String(th.fixedBorder).trim()); + if (m) { + const a0 = m[4] != null && m[4] !== '' ? Number(m[4]) : 1; + const aa = (Number.isFinite(a0) ? Math.max(0, Math.min(1, a0)) : 1) * 0.32; + const t = Math.round(aa * 1000) / 1000; + return 'rgba(' + m[1] + ',' + m[2] + ',' + m[3] + ',' + t + ')'; + } + } + const i = Math.max(0, ov - 1); + const h = (i * 47 + 100) % 360; + return 'hsla(' + h + ', 58%, 52%, 0.32)'; + } + + function normalizeQuizCarryQuestionFromAny(q) { + if (!q) return null; + let text = String(q.text || q.question || q.prompt || q.title || '').trim(); + if (Array.isArray(q.choices) && q.choices.length >= 2) { + const choices = q.choices.map((c) => String(c)); + let ci = Number(q.correctIndex); + if (!Number.isFinite(ci) || ci < 0 || ci >= choices.length) ci = 0; + if (!text) text = 'เลือกคำตอบที่ถูกต้อง — หยิบตัวเลือกไปวางโซนส่งคำตอบ'; + let chSlot = Number(q.countdownHighlightSlot); + if (!Number.isFinite(chSlot) || chSlot < 1 || chSlot > 16) chSlot = null; + else chSlot = Math.floor(chSlot); + let choiceImageUrls = null; + if (Array.isArray(q.choiceImageUrls) && q.choiceImageUrls.length) { + const urls = choices.map((_, idx) => sanitizeQuizCarryImageUrlClient(q.choiceImageUrls[idx])); + if (urls.some((u) => u)) choiceImageUrls = urls; + } + return { text, choices, correctIndex: ci, countdownHighlightSlot: chSlot, choiceImageUrls }; + } + if (!text) return null; + const t = !!q.answerTrue; + return { text, choices: ['จริง', 'เท็จ'], correctIndex: t ? 0 : 1, countdownHighlightSlot: null }; + } + + function buildQuizCarryPoolFromMap(md) { + if (!md || !Array.isArray(md.quizQuestions)) return []; + const out = []; + for (let i = 0; i < md.quizQuestions.length; i++) { + const n = normalizeQuizCarryQuestionFromAny(md.quizQuestions[i]); + if (n) out.push(n); + } + return out; + } + + function getQuizCarryHubTileBounds(md) { + if (!md || md.gameType !== 'quiz_carry') return null; + const grid = md.quizCarryHubArea; + if (!grid || !grid.length) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (let yy = 0; yy < grid.length; yy++) { + const row = grid[yy]; + if (!row) continue; + for (let xx = 0; xx < row.length; xx++) { + if (row[xx] === 1) { + if (xx < minX) minX = xx; + if (yy < minY) minY = yy; + if (xx > maxX) maxX = xx; + if (yy > maxY) maxY = yy; + } + } + } + if (minX === Infinity) return null; + return { minX, minY, maxX, maxY }; + } + + /** จุดกลางโซนตัวเลือกหยิบมาวาง (ค่า grid 1..N = ลำดับ choices) เพื่อวาดข้อความบนแผนที่ */ + function getQuizCarryOptionClusterBounds(md, slot1toN) { + const g = md && md.quizCarryOptionArea; + if (!g || !g.length) return null; + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + let n = 0, sumX = 0, sumY = 0; + for (let yy = 0; yy < g.length; yy++) { + const row = g[yy]; + if (!row) continue; + for (let xx = 0; xx < row.length; xx++) { + if (Number(row[xx]) === slot1toN) { + if (xx < minX) minX = xx; + if (yy < minY) minY = yy; + if (xx > maxX) maxX = xx; + if (yy > maxY) maxY = yy; + sumX += xx + 0.5; + sumY += yy + 0.5; + n++; + } + } + } + if (!n || minX === Infinity) return null; + return { + minX, minY, maxX, maxY, + cx: sumX / n, + cy: sumY / n, + }; + } + + function canvasWordWrapLines(ctx, text, maxWidth) { + const raw = String(text || '').trim(); + if (!raw) return []; + const words = raw.split(/\s+/); + const lines = []; + let cur = words[0]; + for (let i = 1; i < words.length; i++) { + const w = words[i]; + const test = cur + ' ' + w; + if (ctx.measureText(test).width <= maxWidth) cur = test; + else { + lines.push(cur); + cur = w; + } + } + lines.push(cur); + return lines; + } + + function quizCarryOptionHeldByAnyone(optionIdx) { + if (optionIdx == null || optionIdx < 0) return false; + if (me.quizCarryHeld === optionIdx) return true; + for (const o of others.values()) { + if (o && o.quizCarryHeld === optionIdx) return true; + } + return false; + } + + /** สไปรต์ทับช่องโซนกลาง (ม่วง) อย่างน้อย 1 ช่อง */ + function spriteOverlapsQuizCarryHubArea(md, sp) { + if (!md || md.gameType !== 'quiz_carry' || !sp) return false; + const hub = md.quizCarryHubArea; + if (!hub || !hub.length) return false; + const mw = md.width || 20, mh = md.height || 15; + const x0 = sp.x | 0, y0 = sp.y | 0, ww = sp.w | 0, hh = sp.h | 0; + for (let yy = y0; yy < y0 + hh; yy++) { + if (yy < 0 || yy >= mh) continue; + const row = hub[yy]; + if (!row) continue; + for (let xx = x0; xx < x0 + ww; xx++) { + if (xx < 0 || xx >= mw) continue; + if (row[xx] === 1) return true; + } + } + return false; + } + + /** ตัวเลือกหนึ่งข้อ — มีคนถือป้ายอยู่ได้แค่คนเดียว (หยิบซ้ำไม่ได้) */ + function pickQuizCarryTargetOptionIndexAvoidingTaken(preferredIdx) { + const n = quizCarryCurrent && quizCarryCurrent.choices ? quizCarryCurrent.choices.length : 0; + if (n < 1) return null; + let p = preferredIdx | 0; + if (p < 0 || p >= n) p = 0; + if (!quizCarryOptionHeldByAnyone(p)) return p; + const avail = []; + for (let i = 0; i < n; i++) { + if (!quizCarryOptionHeldByAnyone(i)) avail.push(i); + } + if (!avail.length) return null; + return avail[Math.floor(Math.random() * avail.length)]; + } + + /** + * วาดป้ายมุมมน + ขอบเรือง + ข้อความ (รูปแบบเดียวกันทั้งแมปและติดตัว) — สีจาก carryChoicePlaqueThemes[choiceIndex] · รูปจาก plaqueExtra.imageUrl + * @param tether ถ้ามี วาดเส้นขาวจาก (x0,y0) ถึง (x1,y1) ก่อนป้าย + * @param plaqueExtra optional { imageUrl } + */ + function drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, lines, lineH, padY, choiceIndex, tether, plaqueExtra) { + const th = getEffectiveCarryChoicePlaqueThemeForChoice(choiceIndex); + const neon = th.borderMode === 'fixed' ? th.fixedBorder : quizCarryNeonColorForChoice(choiceIndex); + const fillCol = th.fillBg || 'rgba(12, 10, 20, 0.88)'; + const textCol = th.textColor || '#f8f9ff'; + const baseLw = Math.max(0.5, Math.min(8, Number(th.borderWidthPx))); + const imgUrl = plaqueExtra && plaqueExtra.imageUrl ? String(plaqueExtra.imageUrl) : ''; + const elImg = plaqueExtra && plaqueExtra.imageElement; + const imgFromEl = (elImg && elImg.complete && elImg.naturalWidth > 0) ? elImg : null; + const img = !imgFromEl && imgUrl ? getQuizCarryChoiceImageCached(imgUrl) : null; + const drawPlaqueImg = imgFromEl || ((img && img.complete && img.naturalWidth > 0) ? img : null); + const rr = Math.max(6, Math.min(12, Math.min(w, h) * 0.2)); + const simplePreviewPlaque = previewMode && editorEmbedReturn; + function pathBoard() { + ctx.beginPath(); + if (typeof ctx.roundRect === 'function') ctx.roundRect(signX, signY, w, h, rr); + else ctx.rect(signX, signY, w, h); + } + if (!simplePreviewPlaque) { + for (let g = 4; g >= 1; g--) { + ctx.strokeStyle = neon; + ctx.globalAlpha = 0.07 + g * 0.06; + ctx.lineWidth = baseLw + g * 3.2; + pathBoard(); + ctx.stroke(); + } + } + ctx.globalAlpha = 1; + ctx.fillStyle = fillCol; + pathBoard(); + ctx.fill(); + if (drawPlaqueImg) { + ctx.save(); + pathBoard(); + ctx.clip(); + const padImg = Math.max(2, padY * 0.65); + const ix = signX + padImg; + const iy = signY + padImg; + const iw = Math.max(0, w - padImg * 2); + const ih = Math.max(0, h - padImg * 2); + if (iw > 0 && ih > 0) { + const nw = drawPlaqueImg.naturalWidth; + const nh = drawPlaqueImg.naturalHeight; + const ir = nw / nh; + let dw; + let dh; + if (iw / ih > ir) { + dh = ih; + dw = dh * ir; + } else { + dw = iw; + dh = dw / ir; + } + const dx = ix + (iw - dw) / 2; + const dy = iy + (ih - dh) / 2; + ctx.drawImage(drawPlaqueImg, 0, 0, nw, nh, dx, dy, dw, dh); + } + ctx.restore(); + } + ctx.strokeStyle = neon; + if (simplePreviewPlaque) { + ctx.lineWidth = Math.max(1, baseLw * 0.85); + ctx.shadowBlur = 0; + pathBoard(); + ctx.stroke(); + } else { + ctx.lineWidth = baseLw; + ctx.shadowColor = neon; + ctx.shadowBlur = 14; + pathBoard(); + ctx.stroke(); + ctx.shadowBlur = 8; + pathBoard(); + ctx.stroke(); + ctx.shadowBlur = 0; + } + const attachX = signX + w / 2; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = textCol; + const lh = lineH || 12; + if (!simplePreviewPlaque && lines && lines.length) { + ctx.shadowColor = 'rgba(0,0,0,0.55)'; + ctx.shadowBlur = 3; + } + if (lines && lines.length) { + const n = lines.length; + const textBlockH = n * lh; + const y0 = signY + Math.max(padY, (h - textBlockH) / 2); + const firstLineCenterY = y0 + lh / 2; + for (let li = 0; li < n; li++) { + ctx.fillText(lines[li], attachX, firstLineCenterY + li * lh); + } + } + ctx.shadowBlur = 0; + if (tether && Number.isFinite(tether.x0) && Number.isFinite(tether.y0)) { + ctx.strokeStyle = 'rgba(255,255,255,0.95)'; + ctx.lineWidth = 1.5; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.shadowBlur = 0; + ctx.beginPath(); + ctx.moveTo(tether.x0, tether.y0); + ctx.lineTo(tether.x1, tether.y1); + ctx.stroke(); + } + } + + /** + * กล่องป้ายบนจอ (signX, signY, w, h) ให้ตรงกับ drawQuizCarryChoiceLabels สำหรับช่อง idx 0-based + */ + function computeQuizCarryChoicePlaqueScreenRect(ctx, worldToScreen, zoom, idx0Based) { + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return null; + const q = quizCarryCurrent; + const choices = q.choices; + if (!choices || !choices.length || idx0Based < 0 || idx0Based >= choices.length) return null; + const i = idx0Based; + const b = getQuizCarryOptionClusterBounds(mapData, i + 1); + if (!b) return null; + const ts = tileSize * zoom; + const ps = quizCarryPlaqueMapScaleClampedPlay(); + const choiceText = String(choices[i] || '').trim(); + const imgUrl = getQuizCarryPlaqueImageUrlForIndex(q, i); + const gridIdleEl = findQuizCarryGridIdleImgForChoiceIndex(i); + const plaqueExtra = imgUrl ? { imageUrl: imgUrl } + : (gridIdleEl && gridIdleEl.complete && gridIdleEl.naturalWidth ? { imageElement: gridIdleEl } : {}); + if (!choiceText && !plaqueExtra.imageUrl && !plaqueExtra.imageElement) return null; + const tileSpanX = (b.maxX - b.minX + 1); + const tileSpanY = (b.maxY - b.minY + 1); + const minPlaqueW = Math.ceil(Math.max(72, tileSpanX * ts - 16) * ps); + const minPlaqueH = Math.ceil(Math.max(52, tileSpanY * ts - 12) * ps); + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + ctx.save(); + let signX; + let signY; + let w; + let h; + if (!choiceText && (plaqueExtra.imageUrl || plaqueExtra.imageElement)) { + const minW = minPlaqueW; + const minH = minPlaqueH; + signX = sx - minW / 2; + signY = sy - minH / 2; + w = minW; + h = minH; + ctx.restore(); + return { signX, signY, w, h }; + } + const line = choiceText; + const fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps)); + ctx.font = '600 ' + fontPx + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + let maxW = Math.max(56, tileSpanX * ts - 14); + const lineH = fontPx * 1.2; + let textLines = canvasWordWrapLines(ctx, line, maxW); + if (!textLines.length) textLines = [line]; + if (textLines.length * lineH > tileSpanY * ts - 8 && textLines.length > 1) { + const fp2 = Math.max(9, fontPx * 0.85); + ctx.font = '600 ' + fp2 + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + const lh2 = fp2 * 1.2; + maxW = Math.max(48, maxW - 4); + textLines = canvasWordWrapLines(ctx, line, maxW); + if (textLines.length * lh2 > tileSpanY * ts - 8) { + textLines = textLines.slice(0, Math.max(1, Math.floor((tileSpanY * ts - 8) / lh2))); + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + const maxLineW = Math.max(...textLines.map((t) => ctx.measureText(t).width), 40); + const padX = 10; + const padY = 8; + w = Math.ceil(maxLineW + padX * 2); + h = Math.ceil(textLines.length * lh2 + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + signX = sx - w / 2; + signY = sy - h / 2; + ctx.restore(); + return { signX, signY, w, h }; + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + let maxLineW = 0; + for (let ti = 0; ti < textLines.length; ti++) { + const lw = ctx.measureText(textLines[ti]).width; + if (lw > maxLineW) maxLineW = lw; + } + const padX = 10; + const padY = 8; + w = Math.ceil(Math.max(maxLineW, 52) + padX * 2); + h = Math.ceil(textLines.length * lineH + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + signX = sx - w / 2; + signY = sy - h / 2; + ctx.restore(); + return { signX, signY, w, h }; + } + + /** + * ขอบเรืองรอบช่องตัวเลือก (carryEmbedCountdownHighlight + carryEmbedCountdownHighlightColor) + * ขนาดกรอบเท่าป้ายบนแมป (เดียวกับ drawQuizCarryChoiceLabels) ไม่ใช่ทั้ง cluster กริด + */ + function drawQuizCarryEmbedCountdownHighlight(ctx, worldToScreen, zoom, timeMs) { + if (!isQuizCarry() || !mapData) return; + if (mapData.carryEmbedCountdownHighlight === false) return; + const optIdx = getQuizCarryLocalGrabbableOptionIndex(); + if (optIdx == null) return; + const pr = computeQuizCarryChoicePlaqueScreenRect(ctx, worldToScreen, zoom, optIdx); + if (!pr || pr.w < 4 || pr.h < 4) return; + const rx = pr.signX; + const ry = pr.signY; + const rw = pr.w; + const rh = pr.h; + const rr = Math.max(6, Math.min(16, Math.min(rw, rh) * 0.14)); + const colRaw = mapData.carryEmbedCountdownHighlightColor; + const strokeBase = typeof colRaw === 'string' && /^#[0-9a-fA-F]{6}$/.test(colRaw.trim()) ? colRaw.trim() : '#ffb44a'; + const pulse = 0.55 + 0.45 * Math.sin((timeMs || 0) / 180); + ctx.save(); + function pathRr() { + ctx.beginPath(); + if (typeof ctx.roundRect === 'function') ctx.roundRect(rx, ry, rw, rh, rr); + else ctx.rect(rx, ry, rw, rh); + } + pathRr(); + ctx.fillStyle = strokeBase; + ctx.globalAlpha = 0.12 * pulse; + ctx.fill(); + for (let g = 6; g >= 1; g--) { + ctx.strokeStyle = strokeBase; + ctx.globalAlpha = (0.07 + g * 0.055) * pulse; + ctx.lineWidth = 2.5 + g * 4; + pathRr(); + ctx.stroke(); + } + ctx.globalAlpha = 0.98; + ctx.strokeStyle = strokeBase; + ctx.lineWidth = 4; + ctx.shadowColor = strokeBase; + ctx.shadowBlur = 22 * pulse; + pathRr(); + ctx.stroke(); + ctx.shadowBlur = 12 * pulse; + pathRr(); + ctx.stroke(); + ctx.shadowBlur = 0; + ctx.globalAlpha = 1; + ctx.restore(); + } + + function drawQuizCarryChoiceLabels(ctx, worldToScreen, zoom) { + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return; + if (!quizCarryOptionsPickableNow()) return; + const choices = quizCarryCurrent.choices; + if (!choices || !choices.length) return; + const maxSlots = choices.length; + const ts = tileSize * zoom; + const ps = quizCarryPlaqueMapScaleClampedPlay(); + ctx.save(); + for (let i = 0; i < Math.min(choices.length, maxSlots); i++) { + if (quizCarryOptionHeldByAnyone(i)) continue; + const b = getQuizCarryOptionClusterBounds(mapData, i + 1); + if (!b) continue; + const choiceText = String(choices[i] || '').trim(); + const imgUrl = getQuizCarryPlaqueImageUrlForIndex(quizCarryCurrent, i); + const gridIdleEl = findQuizCarryGridIdleImgForChoiceIndex(i); + const plaqueExtra = imgUrl ? { imageUrl: imgUrl } + : (gridIdleEl && gridIdleEl.complete && gridIdleEl.naturalWidth ? { imageElement: gridIdleEl } : {}); + if (!choiceText && !plaqueExtra.imageUrl && !plaqueExtra.imageElement) continue; + const tileSpanX = (b.maxX - b.minX + 1); + const tileSpanY = (b.maxY - b.minY + 1); + const minPlaqueW = Math.ceil(Math.max(72, tileSpanX * ts - 16) * ps); + const minPlaqueH = Math.ceil(Math.max(52, tileSpanY * ts - 12) * ps); + if (!choiceText && (plaqueExtra.imageUrl || plaqueExtra.imageElement)) { + const fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps)); + const lineH = fontPx * 1.2; + const padY = 8; + const minW = minPlaqueW; + const minH = minPlaqueH; + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + drawQuizCarryNeonPlaque(ctx, sx - minW / 2, sy - minH / 2, minW, minH, [], lineH, padY, i, null, plaqueExtra); + continue; + } + const line = choiceText; + const fontPx = Math.max(10, Math.min(24, ts * 0.24 * ps)); + ctx.font = '600 ' + fontPx + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + let maxW = Math.max(56, tileSpanX * ts - 14); + const lineH = fontPx * 1.2; + let textLines = canvasWordWrapLines(ctx, line, maxW); + if (!textLines.length) textLines = [line]; + if (textLines.length * lineH > tileSpanY * ts - 8 && textLines.length > 1) { + const fp2 = Math.max(9, fontPx * 0.85); + ctx.font = '600 ' + fp2 + 'px system-ui, "Segoe UI", "Kanit", sans-serif'; + const lh2 = fp2 * 1.2; + maxW = Math.max(48, maxW - 4); + textLines = canvasWordWrapLines(ctx, line, maxW); + if (textLines.length * lh2 > tileSpanY * ts - 8) { + textLines = textLines.slice(0, Math.max(1, Math.floor((tileSpanY * ts - 8) / lh2))); + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + const maxLineW = Math.max(...textLines.map((t) => ctx.measureText(t).width), 40); + const padX = 10; + const padY = 8; + let w = Math.ceil(maxLineW + padX * 2); + let h = Math.ceil(textLines.length * lh2 + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const signX = sx - w / 2; + const signY = sy - h / 2; + drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, textLines, lh2, padY, i, null, plaqueExtra); + continue; + } + for (let ti = 0; ti < textLines.length; ti++) { + let s = textLines[ti]; + if (ctx.measureText(s).width <= maxW) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxW) s = s.slice(0, -1); + textLines[ti] = s + '…'; + } + let maxLineW = 0; + for (let ti = 0; ti < textLines.length; ti++) { + const lw = ctx.measureText(textLines[ti]).width; + if (lw > maxLineW) maxLineW = lw; + } + const padX = 10; + const padY = 8; + let w = Math.ceil(Math.max(maxLineW, 52) + padX * 2); + let h = Math.ceil(textLines.length * lineH + padY * 2); + if (imgUrl || plaqueExtra.imageElement) { + w = Math.max(w, minPlaqueW); + h = Math.max(h, minPlaqueH); + } + const wx = b.cx * tileSize; + const wy = b.cy * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const signX = sx - w / 2; + const signY = sy - h / 2; + drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, textLines, lineH, padY, i, null, plaqueExtra); + } + ctx.restore(); + } + + function quizCarryHubCellPlay(md, tx, ty) { + const g = md && md.quizCarryHubArea; + return !!(g && g[ty] && g[ty][tx] === 1); + } + + function quizCarryInteractiveCellPlay(md, tx, ty) { + const g = md && md.interactive; + return !!(g && g[ty] && g[ty][tx] === 1); + } + + function quizCarryMapHasAnswerInteractive(md) { + if (!md || !md.interactive || !md.interactive.length) return false; + const h = md.height || 15, w = md.width || 20; + for (let y = 0; y < h; y++) { + const row = md.interactive[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1) return true; + } + } + return false; + } + + /** @returns {number|null} index ใน choices จากช่องหมายเลข 1..N บนแมป */ + function quizCarryOptionIndexAtPlay(md, tx, ty) { + const g = md && md.quizCarryOptionArea; + if (!g || !g[ty]) return null; + const v = g[ty][tx]; + if (v < 1 || v > QUIZ_CARRY_MAX_OPTION_SLOTS) return null; + const idx = v - 1; + const n = quizCarryCurrent && quizCarryCurrent.choices ? quizCarryCurrent.choices.length : 0; + if (n < 1 || idx >= n) return null; + return idx; + } + + /** ร่างตัวละครทับช่องโซนกลาง (ม่วง) — ใช้บล็อกการเดิน */ + function quizCarryFootprintOverlapsHub(px, py) { + if (!mapData || !isQuizCarry()) return false; + const md = mapData; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryHubCellPlay(md, tx, ty)) return true; + } + return false; + } + + /** ยืนข้างนอกแต่ชิดโซนกลาง — ใช้ให้กด F ส่งคำตอบได้โดยไม่ต้องย่ำบนม่วง */ + function quizCarryFootprintAdjacentToHub(md, px, py) { + if (!md) return false; + const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryHubCellPlay(md, tx, ty)) continue; + for (let di = 0; di < dirs.length; di++) { + const nx = tx + dirs[di][0], ny = ty + dirs[di][1]; + if (quizCarryHubCellPlay(md, nx, ny)) return true; + } + } + return false; + } + + function quizCarryCanSubmitAtHub(md, px, py) { + if (!md) return false; + return quizCarryFootprintOverlapsHub(px, py) || quizCarryFootprintAdjacentToHub(md, px, py); + } + + function quizCarryFootprintOverlapsInteractive(md, px, py) { + if (!md) return false; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryInteractiveCellPlay(md, tx, ty)) return true; + } + return false; + } + + function quizCarryFootprintAdjacentToInteractive(md, px, py) { + if (!md) return false; + const dirs = [[1, 0], [-1, 0], [0, 1], [0, -1]]; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (quizCarryInteractiveCellPlay(md, tx, ty)) continue; + for (let di = 0; di < dirs.length; di++) { + const nx = tx + dirs[di][0], ny = ty + dirs[di][1]; + if (quizCarryInteractiveCellPlay(md, nx, ny)) return true; + } + } + return false; + } + + function quizCarryCanSubmitAtInteractiveZone(md, px, py) { + if (!md) return false; + return quizCarryFootprintOverlapsInteractive(md, px, py) || quizCarryFootprintAdjacentToInteractive(md, px, py); + } + + /** ส่งคำตอบ: ถ้ามีโซน interactive (เขียว) ในแมป ใช้โซนนั้น — ไม่มี fallback ชิดโซนกลาง */ + function quizCarryCanSubmitAnswerAt(md, px, py) { + if (!md) return false; + if (quizCarryMapHasAnswerInteractive(md)) { + return quizCarryCanSubmitAtInteractiveZone(md, px, py); + } + return quizCarryCanSubmitAtHub(md, px, py); + } + + /** ข้อความใน HUD / โซนคำถาม — ไม่รวมตัวเลือก (แสดงบนแผนที่ตามจุดสี) */ + function formatQuizCarryQuestionHud(q) { + if (!q) return ''; + return String(q.text || q.question || q.prompt || q.title || '').trim(); + } + + function quizCarryRestoreEmbedPhaseLabel() { + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (!phaseEl || !mapData) return; + phaseEl.textContent = quizCarryMapHasAnswerInteractive(mapData) + ? 'กด F หรือปุ่ม GRAB หยิบบนโซนตัวเลือก · ส่งที่โซน interactive (เขียว) — เล่นพร้อมกันได้' + : 'กด F หรือปุ่ม GRAB หยิบบนโซนตัวเลือก · ส่งเมื่อชิดโซนกลาง (ม่วงเป็นกำแพง) — เล่นพร้อมกันได้'; + } + + function isQuizCarryEmbedCountdownBlockingMovement() { + if (!(previewMode && editorEmbedReturn && isQuizCarry())) return false; + const now = Date.now(); + return (quizCarryEmbedCountdownEndAt > now) || (quizCarryEmbedPreOptionCountdownEndAt > now); + } + + function isQuizCarryEmbedLobbyBlockingMovement() { + return !!(previewMode && editorEmbedReturn && isQuizCarry() && quizCarryPregameActive); + } + + function quizCarryPregameHumanIds() { + const ids = []; + if (myId != null && !isPreviewBotId(myId)) ids.push(String(myId)); + others.forEach((_, id) => { + if (!isPreviewBotId(id)) ids.push(String(id)); + }); + return ids; + } + + function quizCarryPregameBotCount() { + let n = 0; + others.forEach((_, id) => { + if (isPreviewBotId(id)) n++; + }); + return n; + } + + function quizCarryPregameReadyNumerator() { + let n = quizCarryPregameBotCount(); + quizCarryPregameHumanIds().forEach((id) => { + if (quizCarryLobbyReadyMap[id]) n++; + }); + return n; + } + + function quizCarryPregameTotalPlayers() { + return quizCarryPregameHumanIds().length + quizCarryPregameBotCount(); + } + + function isMePlayHost() { + if (myId == null) return false; + if (playHostId == null) return true; + return String(playHostId) === String(myId); + } + + function hideQuizCarryPregameOverlay() { + const ov = document.getElementById('quiz-carry-pregame-overlay'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + } + + function showQuizCarryPregameOverlay() { + const ov = document.getElementById('quiz-carry-pregame-overlay'); + if (ov) { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + updateQuizCarryPregameHud(); + } + + function quizCarrySyncGuestReadyIfNeeded() { + if (!quizCarryPregameActive || myId == null || isMePlayHost()) return; + const sid = String(myId); + if (quizCarryLobbyReadyMap[sid]) return; + quizCarryLobbyReadyMap[sid] = true; + if (socket && socket.connected) socket.emit('quiz-carry-lobby-ready', { ready: true }); + } + + function updateQuizCarryPregameHud() { + if (!quizCarryPregameActive) return; + const st = document.getElementById('quiz-carry-pregame-status'); + const primary = document.getElementById('quiz-carry-pregame-primary'); + const primaryImg = document.getElementById('quiz-carry-pregame-primary-img'); + const num = quizCarryPregameReadyNumerator(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + if (st) { + st.textContent = 'Ready Status : ' + num + '/' + tot; + } + const humans = quizCarryPregameHumanIds(); + const humansReady = humans.length > 0 && humans.every((id) => !!quizCarryLobbyReadyMap[id]); + const meReady = !!(myId && quizCarryLobbyReadyMap[String(myId)]); + if (primaryImg) { + primaryImg.src = humansReady ? '/Game/img/quiz-carry/btn-start.png' : '/Game/img/quiz-carry/btn-ready.png'; + primaryImg.alt = humansReady ? 'START' : 'READY'; + } + if (primary) { + primary.classList.toggle('is-start-phase', humansReady); + primary.classList.toggle('is-read-only', !isMePlayHost()); + primary.classList.toggle('is-pressed', isMePlayHost() && meReady && !humansReady); + primary.disabled = !isMePlayHost(); + primary.setAttribute('aria-pressed', humansReady ? 'false' : (meReady ? 'true' : 'false')); + primary.title = isMePlayHost() + ? (humansReady ? 'START' : (meReady ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + } + + function beginQuizCarryEmbedPregame() { + if (!(previewMode && editorEmbedReturn && isQuizCarry())) return; + quizCarryPregameActive = true; + quizCarryLobbyReadyMap = {}; + quizCarryPregameHumanIds().forEach((id) => { quizCarryLobbyReadyMap[id] = false; }); + showQuizCarryPregameOverlay(); + if (socket && socket.connected) socket.emit('quiz-carry-lobby-sync-request'); + quizCarrySyncGuestReadyIfNeeded(); + updateQuizCarryPregameHud(); + } + + function endQuizCarryEmbedPregameAndStart() { + if (!quizCarryPregameActive) return; + quizCarryPregameActive = false; + hideQuizCarryPregameOverlay(); + quizCarryPickNextQuestion(); + const nowEmb = Date.now(); + if (!(previewMode && editorEmbedReturn && quizCarryEmbedCountdownEndAt > nowEmb) + && !(previewMode && editorEmbedReturn && quizCarryEmbedPreOptionCountdownEndAt > nowEmb)) { + quizCarryRestoreEmbedPhaseLabel(); + } + } + + function quizCarryOptionsPickableNow() { + if (!isQuizCarry() || !quizCarryCurrent) return true; + /* embed: ระหว่าง 3–2–1 รอบสอง (คำถามขึ้นแล้ว) — ยังไม่วาด/ไม่ให้หยิบป้ายตัวเลือก */ + if (previewMode && editorEmbedReturn && quizCarryEmbedPreOptionCountdownEndAt > Date.now()) return false; + if (!quizCarryOptionRevealAt || quizCarryOptionRevealAt <= 0) return true; + return Date.now() >= quizCarryOptionRevealAt; + } + + function quizCarryApplyPhaseTimersForCurrentQuestion() { + if (!quizCarryCurrent) { + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + return; + } + const readMs = Math.max(0, Math.floor(Number(quizCarryCarryTimingMs.carryReadMs)) || 0); + const ansMs = Math.max(1000, Math.floor(Number(quizCarryCarryTimingMs.carryAnswerMs)) || 5000); + const t0 = Date.now(); + if (readMs <= 0) { + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = t0 + ansMs; + } else { + quizCarryOptionRevealAt = t0 + readMs; + quizCarryAnswerCloseAt = quizCarryOptionRevealAt + ansMs; + } + } + + /** embed: หลัง 3–2–1 ก่อนหยิบ — เปิดป้ายตัวเลือกทันที + จับเวลาตอบ carryAnswerMs */ + function quizCarryApplyEmbedAnswerWindowAfterPreOption321() { + if (!quizCarryCurrent) return; + const ansMs = Math.max(1000, Math.floor(Number(quizCarryCarryTimingMs.carryAnswerMs)) || 5000); + const t0 = Date.now(); + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = t0 + ansMs; + } + + function updateQuizCarryCarryPhaseHud() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!isQuizCarry() || !quizCarryCurrent) return; + if (isDetectiveMinigamePlay()) return; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (!phaseEl) return; + if (quizCarryAnswerTimeupAwaitNext && quizCarryAnswerRoundTimeupAdvanceAt > 0) { + const sec = Math.max(0, Math.ceil((quizCarryAnswerRoundTimeupAdvanceAt - Date.now()) / 1000)); + phaseEl.textContent = 'หมดเวลาตอบ — ไปข้อถัดไปใน ' + sec + ' วิ…'; + return; + } + const now = Date.now(); + if (previewMode && editorEmbedReturn && quizCarryEmbedPreOptionCountdownEndAt > 0 && now < quizCarryEmbedPreOptionCountdownEndAt) { + const sec = Math.max(0, Math.ceil((quizCarryEmbedPreOptionCountdownEndAt - now) / 1000)); + phaseEl.textContent = 'เตรียมหยิบคำตอบ — นับ 3·2·1 (' + sec + ' วิ)'; + return; + } + if (quizCarryOptionRevealAt > 0 && now < quizCarryOptionRevealAt) { + const remain = Math.max(0, Math.ceil((quizCarryOptionRevealAt - now) / 1000)); + phaseEl.textContent = 'อ่านคำถาม — ตัวเลือกขึ้นใน ' + remain + ' วิ'; + return; + } + if (quizCarryAnswerCloseAt > now) { + const remain = Math.max(0, Math.ceil((quizCarryAnswerCloseAt - now) / 1000)); + phaseEl.textContent = 'หยิบ/ส่งคำตอบ — เหลือ ' + remain + ' วิ'; + return; + } + quizCarryRestoreEmbedPhaseLabel(); + } + + function tickQuizCarryRoundTimers() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return; + if (quizCarryEmbedCountdownEndAt > Date.now()) return; + if (quizCarryEmbedPreOptionCountdownEndAt > Date.now()) return; + if (quizCarryEmbedPendingQuestion) return; + if (quizCarryAnswerTimeupAwaitNext) return; + if (!quizCarryAnswerCloseAt || quizCarryAnswerCloseAt <= 0) return; + if (Date.now() < quizCarryAnswerCloseAt) return; + quizCarryAnswerCloseAt = 0; + quizCarryOptionRevealAt = 0; + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + }); + renderPlayQuizScoreboard(playLiveQuizScores); + if (isDetectiveMinigamePlay()) { + quizCarryAfterRoundResolved(); + return; + } + quizCarryAnswerTimeupAwaitNext = true; + showQuizCarryAnswerRoundTimeupOverlay(); + updateQuizCarryCarryPhaseHud(); + } + + function resetQuizCarryEmbedCountdownOverlayInners() { + const cq = document.getElementById('quiz-carry-embed-countdown-q'); + const ck = document.getElementById('quiz-carry-embed-countdown-kicker'); + if (cq) { + cq.textContent = ''; + cq.style.display = 'none'; + } + if (ck) { + ck.textContent = 'คำถาม · Question'; + ck.style.display = 'none'; + ck.classList.remove('quiz-carry-embed-countdown-kicker--mission'); + } + } + + /** นับ 3–2–1: ซ่อนคำถามและหัวข้อ — เหลือแค่รูปเลขจาก /Game/img/QUESTION */ + function showQuizCarryEmbedCountdownDigitsOnlyChrome() { + const cq = document.getElementById('quiz-carry-embed-countdown-q'); + const ck = document.getElementById('quiz-carry-embed-countdown-kicker'); + if (cq) { + cq.textContent = ''; + cq.style.display = 'none'; + } + if (ck) { + ck.textContent = ''; + ck.style.display = 'none'; + ck.classList.remove('quiz-carry-embed-countdown-kicker--mission'); + } + } + + function hideQuizCarryEmbedCountdownOverlay() { + resetQuizCarryEmbedCountdownLayoutDom(); + const ov = document.getElementById('quiz-carry-embed-countdown'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + resetQuizCarryEmbedCountdownOverlayInners(); + } + + function tickQuizCarryEmbedCountdown() { + if (!previewMode || !editorEmbedReturn || !isQuizCarry()) return; + const now = Date.now(); + const ov = document.getElementById('quiz-carry-embed-countdown'); + const numEl = document.getElementById('quiz-carry-embed-countdown-num'); + + if (quizCarryCurrent && quizCarryEmbedPreOptionCountdownEndAt > 0) { + if (!numEl) return; + if (now >= quizCarryEmbedPreOptionCountdownEndAt) { + quizCarryEmbedPreOptionCountdownEndAt = 0; + quizCarryEmbedPreOptionCountdownStartAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + quizCarryApplyEmbedAnswerWindowAfterPreOption321(); + quizCarryRestoreEmbedPhaseLabel(); + syncQuizCarryEmbedQuestionStrip(); + updateQuizCarryCarryPhaseHud(); + return; + } + const elapsedPre = now - quizCarryEmbedPreOptionCountdownStartAt; + const nPre = 3 - Math.min(2, Math.floor(elapsedPre / 1000)); + setCountdown321QuestionAssetGraphic(numEl, Math.max(1, Math.min(3, nPre))); + if (ov) { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + syncQuizCarryEmbedCountdownLayout(); + showQuizCarryEmbedCountdownDigitsOnlyChrome(); + syncQuizCarryEmbedQuestionStrip(); + return; + } + + if (!quizCarryEmbedPendingQuestion || quizCarryEmbedCountdownEndAt <= 0) return; + if (now >= quizCarryEmbedCountdownEndAt) { + const pq = quizCarryEmbedPendingQuestion; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedCountdownStartAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + if (!pq) return; + quizCarryCurrent = pq; + preloadQuizCarryChoiceImages(pq); + playQuizText = formatQuizCarryQuestionHud(pq); + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + quizCarryRestoreEmbedPhaseLabel(); + others.forEach((o) => { + if (!o) return; + o.botQuizCarryPathfindAfter = 0; + }); + syncQuizCarryEmbedQuestionStrip(); + quizCarryEmbedPreOptionCountdownStartAt = Date.now(); + quizCarryEmbedPreOptionCountdownEndAt = quizCarryEmbedPreOptionCountdownStartAt + 3000; + updateQuizCarryCarryPhaseHud(); + return; + } + const elapsed = now - quizCarryEmbedCountdownStartAt; + const n = 3 - Math.min(2, Math.floor(elapsed / 1000)); + setCountdown321QuestionAssetGraphic(numEl, Math.max(1, Math.min(3, n))); + if (ov) { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + syncQuizCarryEmbedCountdownLayout(); + showQuizCarryEmbedCountdownDigitsOnlyChrome(); + syncQuizCarryEmbedQuestionStrip(); + } + + function quizCarryPickNextQuestion() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!quizCarryPool.length) { + quizCarryCurrent = null; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownEndAt = 0; + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + playQuizText = 'ไม่มีคำถาม — ใส่ quizQuestions ในแมป (choices + correctIndex) หรือตั้งใน Admin'; + return; + } + let q; + let guard = 0; + do { + q = quizCarryPool[Math.floor(Math.random() * quizCarryPool.length)]; + guard++; + } while (guard < 12 && quizCarryPool.length > 1 && quizCarryCurrent && q && q.text === quizCarryCurrent.text); + if (previewMode && editorEmbedReturn) { + quizCarryEmbedPendingQuestion = q; + preloadQuizCarryChoiceImages(q); + quizCarryEmbedCountdownStartAt = Date.now(); + quizCarryEmbedCountdownEndAt = quizCarryEmbedCountdownStartAt + 3000; + quizCarryEmbedPreOptionCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownEndAt = 0; + quizCarryCurrent = null; + playQuizText = ''; + clearQuizMapQuestionCarryFeedback(); + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + o.botPathStuckTicks = 0; + /* ต้องใช้ performance.now() ช่วงกับ stepQuizCarryPreviewBots — ห้ามใช้ Date.now() epoch ไม่งั้นบอทค้าง pathfind ตลอด */ + o.botQuizCarryPathfindAfter = 0; + }); + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = '…'; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'รอเริ่มคำถาม — นับถอยหลัง 3 · 2 · 1'; + tickQuizCarryEmbedCountdown(); + syncQuizCarryEmbedQuestionStrip(); + return; + } + quizCarryCurrent = q; + preloadQuizCarryChoiceImages(q); + playQuizText = formatQuizCarryQuestionHud(q); + clearQuizMapQuestionCarryFeedback(); + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + }); + quizCarryApplyPhaseTimersForCurrentQuestion(); + } + + /** หลังจบข้อหนึ่งข้อ (ถูกหรือหมดเวลา): พรีวิวนับรอบจนครบ carrySessionLength แล้วจบเซสชัน */ + function quizCarryAfterRoundResolved() { + if (!isQuizCarry()) return; + if (isDetectiveMinigamePlay()) { + finishDetectiveMinigameAndReturnLobby(); + return; + } + if (!previewMode) { + quizCarryPickNextQuestion(); + return; + } + quizCarryRoundsCompleted++; + const lim = quizCarrySessionLength; + if (lim > 0 && quizCarryRoundsCompleted >= lim) { + showQuizCarrySessionCompleteOverlay(); + return; + } + quizCarryPickNextQuestion(); + } + + function playQuizCarryAvatarUrlForMission(characterId) { + if (!characterId) return ''; + return BASE + '/img/characters/' + encodeURIComponent(String(characterId)) + '_down.png'; + } + + /** อวาตาร์สรุปภารกิจ / DOM — ใช้สี gen เดียวกับในเกม (เลเยอร์ + tint) */ + function applyQuizCarryRankAvatarTint(imgEl, characterId, peerId) { + if (!imgEl || !characterId) return; + let attempts = 0; + const fallbackUrl = playQuizCarryAvatarUrlForMission(characterId); + const tick = () => { + attempts++; + const nowT = Date.now(); + const rawImg = getCharacterFrame(characterId, 'down', nowT, false) || getCharacterImg(characterId, 'down'); + if (!rawImg) { + if (fallbackUrl) imgEl.src = fallbackUrl; + return; + } + if ((!rawImg.complete || !rawImg.naturalWidth) && attempts < 28) { + if (fallbackUrl) imgEl.src = fallbackUrl + '?p=' + String(attempts); + setTimeout(tick, 90); + return; + } + if (!rawImg.naturalWidth) { + if (fallbackUrl) imgEl.src = fallbackUrl; + return; + } + const tint = (peerId != null && myId != null && String(peerId) === String(myId)) + ? (me.playTint || playTintFromPeerId(String(peerId))) + : playTintFromPeerId(peerId != null ? String(peerId) : String(characterId)); + const out = getPlayTintedAvatarSource(rawImg, characterId, 'down', nowT, false, tint); + if (out && out.tagName === 'CANVAS' && out.width > 0) { + try { + imgEl.src = out.toDataURL('image/png'); + return; + } catch (e) { /* ต่อไปลอง walk หรือ URL */ } + } + if (out && out.src && out !== rawImg) { + imgEl.src = out.src; + return; + } + if (attempts < 28 && playCharLayerDiscoveryPending(characterId)) { + setTimeout(tick, 120); + return; + } + imgEl.src = rawImg.src || fallbackUrl; + }; + tick(); + } + + /** เกรดจากค่าเฉลี่ยคะแนนทีม (0–100 หลังปัด) — A 80–100, B 60–79, C 0–59 */ + function quizCarryGradeFromTeamAverage(avgRounded) { + if (avgRounded >= 80) return 'A'; + if (avgRounded >= 60) return 'B'; + return 'C'; + } + + function quizCarryRollCardRewardForGrade(grade) { + const u = Math.random() * 100; + if (grade === 'A') { + if (u < 30) return { th: 'การ์ดชี้คนร้าย', en: 'Culprit hint card' }; + if (u < 80) return { th: 'การ์ด Rare', en: 'Rare card' }; + return { th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'B') { + if (u < 20) return { th: 'การ์ดชี้คนร้าย', en: 'Culprit hint card' }; + if (u < 50) return { th: 'การ์ด Rare', en: 'Rare card' }; + return { th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'C') { + if (u < 20) return { th: 'การ์ด Rare', en: 'Rare card' }; + return { th: 'การ์ดธรรมดา', en: 'Common card' }; + } + return null; + } + + function quizCarryBuildMissionRankList() { + const ranks = []; + if (myId != null) { + ranks.push({ + id: myId, + nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', + score: Math.max(0, Number(playLiveQuizScores[myId]) || 0), + characterId: me.characterId || getPlayCharacterId(), + }); + } + others.forEach((o, id) => { + if (!o) return; + ranks.push({ + id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : String(id), + score: Math.max(0, Number(playLiveQuizScores[id]) || 0), + characterId: o.characterId || null, + }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + return ranks.slice(0, 5); + } + + function cancelEmbedPreviewLobbyReturnTimer() { + if (embedPreviewLobbyReturnTimer != null) { + clearTimeout(embedPreviewLobbyReturnTimer); + embedPreviewLobbyReturnTimer = null; + } + } + + function cancelQuizCarryResultEndAfterTimeup() { + if (quizCarryResultEndTimer != null) { + clearTimeout(quizCarryResultEndTimer); + quizCarryResultEndTimer = null; + } + cancelEmbedPreviewLobbyReturnTimer(); + } + + function cancelQuizCarrySessionCompleteResultToSummary() { + if (quizCarrySessionCompleteResultToSummaryT != null) { + clearTimeout(quizCarrySessionCompleteResultToSummaryT); + quizCarrySessionCompleteResultToSummaryT = null; + } + } + + function cancelQuizCarryAnswerRoundTimeupAuto() { + if (quizCarryAnswerRoundTimeupAutoT != null) { + clearTimeout(quizCarryAnswerRoundTimeupAutoT); + quizCarryAnswerRoundTimeupAutoT = null; + } + quizCarryAnswerRoundTimeupAdvanceAt = 0; + } + + function quizCarrySetAnswerRoundTimeupVisualActive(on) { + try { + if (on) document.documentElement.classList.add('quiz-carry-answer-timeup-active'); + else document.documentElement.classList.remove('quiz-carry-answer-timeup-active'); + } catch (e) { /* ignore */ } + } + + /** หลังภาพ result-complete / gameover (editor embed + preview): รอ 5s แล้วไปล็อบบี้ห้องพรีวิว */ + function scheduleEmbedPreviewReturnToLobbyAfterResultEnd() { + cancelEmbedPreviewLobbyReturnTimer(); + if (!(previewMode && editorEmbedReturn)) return; + embedPreviewLobbyReturnTimer = setTimeout(function () { + embedPreviewLobbyReturnTimer = null; + window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + }, 5000); + } + + /** มีผู้เล่นอย่างน้อยหนึ่งคนได้คะแนน > 0 (= ส่งคำตอบถูกที่ฮับอย่างน้อย 1 ครั้ง; คะแนนต่อข้อคงที่ QUIZ_CARRY_POINTS_PER_CORRECT) */ + function quizCarryAnyPlayerHasCorrectAnswer() { + if (!playLiveQuizScores || typeof playLiveQuizScores !== 'object') return false; + const keys = Object.keys(playLiveQuizScores); + for (let i = 0; i < keys.length; i++) { + if (Math.max(0, Number(playLiveQuizScores[keys[i]]) || 0) > 0) return true; + } + return false; + } + + function hideQuizCarryResultEndLayer() { + cancelEmbedPreviewLobbyReturnTimer(); + const el = document.getElementById('quiz-carry-result-end-layer'); + if (!el) return; + el.classList.add('is-hidden'); + el.setAttribute('aria-hidden', 'true'); + } + + function showQuizCarryResultEndLayer(useComplete) { + const el = document.getElementById('quiz-carry-result-end-layer'); + const img = document.getElementById('quiz-carry-result-end-img'); + if (!el || !img) return; + const fname = useComplete ? 'result-complete.png' : 'result-gameover.png'; + if (isSpaceShooterMissionUiMapPlay()) { + img.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/quiz-carry/' + fname + '?v=' + String(Date.now()); + }; + img.src = violentCrimeAssetUrl(fname) + '?v=' + String(Date.now()); + } else { + img.onerror = null; + img.src = BASE + '/img/quiz-carry/' + fname + '?v=' + String(Date.now()); + } + el.classList.remove('is-hidden'); + el.setAttribute('aria-hidden', 'false'); + } + + /** Embed editor: หลัง timeup บนโต๊ะ — รอ 5 วิ แล้วโชว์ result-complete / result-gameover (ใช้ร่วม quiz_carry + crown) */ + function scheduleEmbedDeskResultAfterDelay(okResolver) { + cancelQuizCarryAnswerRoundTimeupAuto(); + cancelQuizCarryResultEndAfterTimeup(); + hideQuizCarryResultEndLayer(); + const resolve = typeof okResolver === 'function' ? okResolver : function () { return !!okResolver; }; + quizCarryResultEndTimer = setTimeout(function () { + quizCarryResultEndTimer = null; + let anyOk = false; + try { + anyOk = !!resolve(); + } catch (e) { + anyOk = false; + } + hideQuizCarryTimeupOnDeskLayer(); + showQuizCarryResultEndLayer(anyOk); + scheduleEmbedPreviewReturnToLobbyAfterResultEnd(); + }, 5000); + } + + function hideQuizCarryTimeupOnDeskLayer() { + cancelQuizCarryAnswerRoundTimeupAuto(); + cancelQuizCarryResultEndAfterTimeup(); + quizCarrySetAnswerRoundTimeupVisualActive(false); + const el = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!el) { + quizCarryAnswerTimeupAwaitNext = false; + return; + } + el.classList.remove('qc-timeup-desk--interactive'); + quizCarryAnswerTimeupAwaitNext = false; + const anchor = el.querySelector('.qc-timeup-desk-anchor') || el.querySelector('.qc-timeup-desk-inner'); + if (anchor) { + anchor.style.left = ''; + anchor.style.top = ''; + anchor.style.width = ''; + anchor.style.height = ''; + } + el.classList.add('is-hidden'); + el.setAttribute('aria-hidden', 'true'); + } + + /** หมดเวลาตอบ (carryAnswerMs) — โชว์ TIME'S UP บนโซนคำถาม ~3s แล้วไปข้อถัดไป (ไม่ใช่ flow 5s → result ของ embed จบภารกิจ) */ + function showQuizCarryAnswerRoundTimeupOverlay() { + if (!isQuizCarry()) return; + cancelQuizCarryAnswerRoundTimeupAuto(); + const el = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!el) return; + const img = document.getElementById('quiz-carry-timeup-txt-img'); + if (img) { + img.onerror = null; + img.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + } + quizCarrySetAnswerRoundTimeupVisualActive(true); + quizCarryAnswerRoundTimeupAdvanceAt = Date.now() + 3000; + el.classList.add('qc-timeup-desk--interactive'); + el.classList.remove('is-hidden'); + el.setAttribute('aria-hidden', 'false'); + try { + syncQuizCarryTimeupDeskLayerPosition(); + } catch (e) { /* ignore */ } + quizCarryAnswerRoundTimeupAutoT = setTimeout(function () { + quizCarryAnswerRoundTimeupAutoT = null; + dismissQuizCarryAnswerRoundTimeupAndContinue(); + }, 3000); + } + + function dismissQuizCarryAnswerRoundTimeupAndContinue() { + if (!quizCarryAnswerTimeupAwaitNext) return; + cancelQuizCarryAnswerRoundTimeupAuto(); + quizCarryAnswerTimeupAwaitNext = false; + hideQuizCarryTimeupOnDeskLayer(); + quizCarryAfterRoundResolved(); + } + + /** หลังกดรับหลักฐาน (embed preview) — แสดง timeup กลางแคนวาส (Jumper mnptfts2 = img/Jumper/result-timeup.png) แยกจากป๊อปสรุปคะแนน + * @param {function():boolean} [embedResultOkFn] — ถ้าไม่ส่ง = ใช้คะแนน quiz_carry (คนตอบถูก); ส่งเมื่อ crown เพื่อเลือก complete vs gameover */ + function showQuizCarryTimeupOnDeskLayer(embedResultOkFn) { + cancelQuizCarryAnswerRoundTimeupAuto(); + quizCarrySetAnswerRoundTimeupVisualActive(false); + const el = document.getElementById('quiz-carry-timeup-desk-layer'); + if (!el) return; + // Last Light (Gauntlet Crown): skip TIME'S UP desk graphic; keep embed 5s → result flow. + if (isGauntletCrownHeistMapPlay()) { + if (previewMode && editorEmbedReturn) { + scheduleEmbedDeskResultAfterDelay( + typeof embedResultOkFn === 'function' ? embedResultOkFn : quizCarryAnyPlayerHasCorrectAnswer + ); + } + return; + } + const img = document.getElementById('quiz-carry-timeup-txt-img'); + if (img) { + if (isJumpSurviveMissionUiMapPlay()) { + img.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + }; + img.src = jumperAssetUrl('result-timeup.png') + '?v=' + String(Date.now()); + } else if (isSpaceShooterMissionUiMapPlay()) { + img.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + }; + img.src = violentCrimeAssetUrl('result-timeup.png') + '?v=' + String(Date.now()); + } else { + img.onerror = null; + img.src = BASE + '/img/quiz-carry/timeup-txt.png?v=' + String(Date.now()); + } + } + el.classList.remove('is-hidden'); + el.setAttribute('aria-hidden', 'false'); + try { + syncQuizCarryTimeupDeskLayerPosition(); + } catch (e) { /* ignore */ } + if (previewMode && editorEmbedReturn) { + scheduleEmbedDeskResultAfterDelay(embedResultOkFn || quizCarryAnyPlayerHasCorrectAnswer); + } + } + + function showQuizCarryMissionSummaryOverlay(opts) { + opts = opts || {}; + const forceF = !!opts.forceGradeF; + const overlay = document.getElementById('quiz-carry-mission-overlay'); + if (!overlay) return; + hideQuizCarryTimeupOnDeskLayer(); + const ranks = quizCarryBuildMissionRankList(); + const parts = ranks.map((r) => Math.max(0, Number(r.score) || 0)); + const total = parts.reduce((a, b) => a + b, 0); + const n = ranks.length; + const avgForGrade = n ? Math.min(100, Math.max(0, Math.round(total / n))) : 0; + const grade = forceF ? 'F' : quizCarryGradeFromTeamAverage(avgForGrade); + const reward = grade === 'F' ? null : quizCarryRollCardRewardForGrade(grade); + + const rowEl = document.getElementById('qc-mission-rank-row'); + const totalEl = document.getElementById('qc-mission-total-line'); + const gradeEl = document.getElementById('qc-mission-grade'); + const cardTextEl = document.getElementById('qc-mission-card-text'); + const checkEl = document.getElementById('qc-mission-check'); + const successTxt = document.getElementById('qc-mission-success-txt'); + const rankTagLabels = ['[1st]', '[2nd]', '[3rd]', '[4th]', '[5th]']; + if (rowEl) { + rowEl.innerHTML = ''; + ranks.forEach((r, idx) => { + const rank = idx + 1; + const cell = document.createElement('div'); + cell.className = 'qc-mission-rank-cell'; + const tag = document.createElement('div'); + tag.className = 'qc-mission-rank-tag'; + tag.textContent = rankTagLabels[rank - 1] || ('[' + rank + ']'); + const frame = document.createElement('div'); + frame.className = 'qc-mission-rank-frame'; + const img = document.createElement('img'); + img.className = 'qc-mission-rank-avatar'; + img.alt = ''; + applyQuizCarryRankAvatarTint(img, r.characterId, r.id); + img.onerror = function () { + this.onerror = null; + this.src = 'data:image/svg+xml,' + encodeURIComponent('?'); + }; + frame.appendChild(img); + const nick = document.createElement('div'); + nick.className = 'qc-mission-rank-nick'; + nick.textContent = r.nickname; + nick.title = r.nickname; + const sc = document.createElement('div'); + sc.className = 'qc-mission-rank-score'; + sc.textContent = String(r.score); + cell.appendChild(tag); + cell.appendChild(frame); + cell.appendChild(nick); + cell.appendChild(sc); + rowEl.appendChild(cell); + }); + } + if (totalEl) { + totalEl.textContent = ''; + if (!n) { + totalEl.textContent = '—'; + } else { + const lab = document.createElement('span'); + lab.textContent = 'คะแนนรวม '; + const nums = document.createElement('span'); + nums.className = 'qc-mission-total-nums'; + nums.textContent = '(' + parts.join('+') + ') = ' + total; + const tail = document.createElement('span'); + tail.textContent = ' · เฉลี่ยทีม ' + avgForGrade + '/100'; + totalEl.appendChild(lab); + totalEl.appendChild(nums); + totalEl.appendChild(tail); + } + } + if (gradeEl) { + gradeEl.textContent = grade; + gradeEl.className = 'qc-mission-grade qc-mission-grade--' + String(grade).toLowerCase(); + } + const okMission = grade !== 'F'; + if (checkEl) checkEl.style.display = okMission ? '' : 'none'; + if (successTxt) { + successTxt.style.display = okMission ? '' : 'none'; + if (okMission) successTxt.textContent = 'ภารกิจสำเร็จ'; + } + if (cardTextEl) { + cardTextEl.textContent = ''; + if (grade === 'F') { + const pl = document.createElement('div'); + pl.className = 'qc-mission-reward-plaque qc-mission-reward-plaque--muted'; + const t = document.createElement('div'); + t.className = 'qc-mission-reward-plaque-title'; + t.textContent = 'ไม่ได้รับการ์ด'; + const s = document.createElement('div'); + s.className = 'qc-mission-reward-plaque-sub'; + s.textContent = 'เกรด F'; + pl.appendChild(t); + pl.appendChild(s); + cardTextEl.appendChild(pl); + } else if (reward) { + const pl = document.createElement('div'); + pl.className = 'qc-mission-reward-plaque'; + const t = document.createElement('div'); + t.className = 'qc-mission-reward-plaque-title'; + t.textContent = reward.th; + const s = document.createElement('div'); + s.className = 'qc-mission-reward-plaque-sub'; + s.textContent = reward.en; + pl.appendChild(t); + pl.appendChild(s); + cardTextEl.appendChild(pl); + } + } + overlay.classList.remove('is-hidden'); + const btn = document.getElementById('btn-quiz-carry-mission-done'); + if (btn) { + btn.onclick = function () { + cancelQuizCarrySessionCompleteResultToSummary(); + overlay.classList.add('is-hidden'); + window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + }; + } + } + + function showQuizCarrySessionCompleteOverlay() { + if (quizCarrySessionEnded) return; + quizCarrySessionEnded = true; + quizCarryPregameActive = false; + hideQuizCarryPregameOverlay(); + quizCarryCurrent = null; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownEndAt = 0; + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + hideQuizCarryEmbedCountdownOverlay(); + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + }); + playQuizText = 'ครบชุดคำถามแล้ว'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'ครบ ' + String(quizCarryRoundsCompleted) + ' ข้อ'; + const missionOv = document.getElementById('quiz-carry-mission-overlay'); + if (missionOv && quizCarryUseMissionSummaryOverlay) { + cancelQuizCarrySessionCompleteResultToSummary(); + hideQuizCarryResultEndLayer(); + const summaryOpts = { forceGradeF: !!(playQuizPlayerLocal && playQuizPlayerLocal.eliminated) }; + const splashRanks = quizCarryBuildMissionRankList(); + const splashParts = splashRanks.map((r) => Math.max(0, Number(r.score) || 0)); + const splashTotal = splashParts.reduce((a, b) => a + b, 0); + const splashN = splashRanks.length; + const splashAvg = splashN ? Math.min(100, Math.max(0, Math.round(splashTotal / splashN))) : 0; + const splashGrade = summaryOpts.forceGradeF ? 'F' : quizCarryGradeFromTeamAverage(splashAvg); + const useCompleteSplash = splashGrade !== 'F'; + showQuizCarryResultEndLayer(useCompleteSplash); + quizCarrySessionCompleteResultToSummaryT = setTimeout(function () { + quizCarrySessionCompleteResultToSummaryT = null; + hideQuizCarryResultEndLayer(); + showQuizCarryMissionSummaryOverlay(summaryOpts); + }, QUIZ_CARRY_SESSION_END_SPLASH_MS); + renderPlayQuizScoreboard(playLiveQuizScores); + return; + } + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) titleEl.textContent = 'เกมจบ · Game over'; + msgEl.textContent = 'ครบจำนวนคำถามที่ตั้งไว้ (' + String(quizCarryRoundsCompleted) + ' ข้อ) · Quiz carry session finished'; + listEl.innerHTML = ''; + const ranks = []; + if (myId != null) { + ranks.push({ + id: myId, + nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', + score: Math.max(0, Number(playLiveQuizScores[myId]) || 0), + }); + } + others.forEach((o, id) => { + if (!o) return; + ranks.push({ + id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : String(id), + score: Math.max(0, Number(playLiveQuizScores[id]) || 0), + }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + if (tryFinishDetectiveMinigameAndReturnLobby()) return; + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + renderPlayQuizScoreboard(playLiveQuizScores); + } + + /** Toast กลางบน (หยิบ / ถูก / ผิด / คำใบ้) — ปิดการแสดงตามคำขอ UX */ + function showQuizCarryToast(msg, ok) { + if (typeof window.__quizCarryToastT === 'number') clearTimeout(window.__quizCarryToastT); + window.__quizCarryToastT = null; + const el = document.getElementById('play-quiz-feedback'); + if (!el) return; + el.classList.add('is-hidden'); + el.className = 'play-quiz-feedback'; + el.textContent = ''; + } + + /** + * หยิบ/ส่งคำตอบ — ต้องกด F (fromKey) สำหรับผู้เล่น · บอท preview เรียกแบบ silent เมื่อหยุดเดินแล้ว + * @returns {boolean} ทำ action สำเร็จหรือไม่ + */ + function tryQuizCarryInteractionForPlayer(actorId, ox, oy, opts) { + opts = opts || {}; + if (quizCarryPregameActive) return false; + if (quizCarrySessionEnded) return false; + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return false; + const md = mapData; + const footprint = quizTilesFootprintPlay(ox, oy); + const ent = actorId === myId ? me : others.get(actorId); + if (!ent) return false; + let pickupIdx = null; + for (const k of footprint) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + const opt = quizCarryOptionIndexAtPlay(md, tx, ty); + if (opt != null) pickupIdx = pickupIdx === null ? opt : pickupIdx; + } + const canSubmitAnswer = quizCarryCanSubmitAnswerAt(md, ox, oy); + const correct = quizCarryCurrent.correctIndex; + let held = ent.quizCarryHeld; + if (held != null && canSubmitAnswer) { + if (!quizCarryOptionsPickableNow()) return false; + ent.quizCarryHeld = null; + const ok = held === correct; + if (ok) { + playLiveQuizScores[actorId] = (playLiveQuizScores[actorId] || 0) + QUIZ_CARRY_POINTS_PER_CORRECT; + const lim = previewMode && isQuizCarry() ? quizCarrySessionLength : 0; + const willEnd = lim > 0 && (quizCarryRoundsCompleted + 1) >= lim; + const showMapFb = !opts.silent && actorId === myId; + if (showMapFb) showQuizMapQuestionCarryFeedback(true); + if (!opts.silent) { + const pts = QUIZ_CARRY_POINTS_PER_CORRECT; + showQuizCarryToast(willEnd ? ('ถูกต้อง +' + pts + ' แต้ม — ครบชุดคำถาม') : ('ถูกต้อง +' + pts + ' แต้ม — สุ่มคำถามใหม่'), true); + } + me.quizCarryHeld = null; + others.forEach((o) => { if (o) o.quizCarryHeld = null; }); + const afterOk = () => { quizCarryAfterRoundResolved(); }; + if (showMapFb) { + window.setTimeout(afterOk, 560); + } else { + afterOk(); + } + } else { + if (!opts.silent && actorId === myId) { + showQuizMapQuestionCarryFeedback(false); + } + if (!opts.silent) { + showQuizCarryToast('ผิด — คืนป้ายที่โซนตัวเลือกแล้ว กด F หยิบใหม่ได้', false); + } + } + renderPlayQuizScoreboard(playLiveQuizScores); + return true; + } + if (held == null && pickupIdx != null) { + if (!quizCarryOptionsPickableNow()) return false; + if (quizCarryOptionHeldByAnyone(pickupIdx)) { + if (opts.fromKey && actorId === myId && !opts.silent) { + showQuizCarryToast('ตัวเลือกนี้มีคนถืออยู่แล้ว — เลือกข้ออื่น', false); + } + return false; + } + ent.quizCarryHeld = pickupIdx; + const label = (quizCarryCurrent.choices && quizCarryCurrent.choices[pickupIdx]) || String(pickupIdx); + if (!opts.silent) { + showQuizCarryToast('หยิบ: ' + label, true); + } + return true; + } + if (opts.fromKey && actorId === myId && !opts.silent) { + const t = Date.now(); + if (t - (me.quizCarryHintT || 0) > 2200) { + me.quizCarryHintT = t; + if (held != null && !canSubmitAnswer) { + showQuizCarryToast(quizCarryMapHasAnswerInteractive(md) + ? 'ถือป้ายอยู่ — ยืนที่โซน interactive (เขียว) หรือชิดขอบแล้วกด F เพื่อส่ง' + : 'ถือป้ายอยู่ — ยืนชิดโซนกลาง (ม่วง) แล้วกด F เพื่อส่ง', false); + } else { + showQuizCarryToast(quizCarryMapHasAnswerInteractive(md) + ? 'ยืนโซนตัวเลือกแล้วกด F หยิบ · ยืนโซนเขียว (interactive) แล้วกด F ส่งเมื่อถือป้าย' + : 'ยืนโซนตัวเลือกแล้วกด F หยิบ · ยืนชิดโซนกลางแล้วกด F ส่งเมื่อถือป้าย', false); + } + } + } + return false; + } + + function quizCarryGrabCoreGatesOk() { + if (!isQuizCarry() || !mapData || !quizCarryCurrent) return false; + if (quizCarryPregameActive || quizCarrySessionEnded) return false; + if (myId == null) return false; + if (isQuizCarryEmbedCountdownBlockingMovement()) return false; + if (!quizCarryOptionsPickableNow()) return false; + return true; + } + + /** ดัชนีตัวเลือก 0-based ที่ผู้เล่นท้องถิ่นยืนอยู่และหยิบได้ — ไม่มีคืน null */ + function getQuizCarryLocalGrabbableOptionIndex() { + if (!quizCarryGrabCoreGatesOk()) return null; + if (me.quizCarryHeld != null) return null; + const md = mapData; + const footprint = quizTilesFootprintPlay(me.x, me.y); + let pickupIdx = null; + for (const k of footprint) { + const p = k.split(','); + const tx = +p[0]; + const ty = +p[1]; + const opt = quizCarryOptionIndexAtPlay(md, tx, ty); + if (opt != null) pickupIdx = pickupIdx === null ? opt : pickupIdx; + } + if (pickupIdx == null || quizCarryOptionHeldByAnyone(pickupIdx)) return null; + return pickupIdx; + } + + /** ยืนบนช่องตัวเลือกและหยิบได้ */ + function quizCarryGrabPickupAvailable() { + return getQuizCarryLocalGrabbableOptionIndex() != null; + } + + /** ถือป้ายแล้วยืนโซนส่งได้ */ + function quizCarryGrabSubmitAvailable() { + if (!quizCarryGrabCoreGatesOk()) return false; + if (me.quizCarryHeld == null) return false; + return quizCarryCanSubmitAnswerAt(mapData, me.x, me.y); + } + + /** true = กด F / ปุ่ม จะหยิบหรือส่งได้ทันที */ + function quizCarryGrabInteractionAvailable() { + return quizCarryGrabPickupAvailable() || quizCarryGrabSubmitAvailable(); + } + + function syncQuizCarryGrabButton() { + const btn = document.getElementById('quiz-carry-grab-btn'); + if (!btn) return; + if (!isQuizCarry() || !mapData) { + btn.classList.add('is-hidden'); + btn.classList.remove('quiz-carry-grab-btn--active'); + btn.classList.remove('quiz-carry-grab-btn--place'); + btn.setAttribute('aria-disabled', 'true'); + return; + } + const grabSrc = BASE + '/img/quiz-carry/btn-grab.png'; + const placeSrc = BASE + '/img/quiz-carry/btn-drop.png'; + const img = btn.querySelector('img'); + btn.classList.remove('is-hidden'); + const active = quizCarryGrabInteractionAvailable(); + btn.classList.toggle('quiz-carry-grab-btn--active', active); + btn.setAttribute('aria-disabled', active ? 'false' : 'true'); + btn.style.pointerEvents = active ? '' : 'none'; + if (img) { + if (active && quizCarryGrabSubmitAvailable()) { + img.src = placeSrc; + btn.classList.add('quiz-carry-grab-btn--place'); + btn.title = 'ส่ง / วางคำตอบ — เหมือนกด F'; + btn.setAttribute('aria-label', 'ส่งหรือวางคำตอบ'); + } else { + img.src = grabSrc; + btn.classList.remove('quiz-carry-grab-btn--place'); + btn.title = 'หยิบตัวเลือก — เหมือนกด F'; + btn.setAttribute('aria-label', 'หยิบตัวเลือก (Grab)'); + } + } + } + + /** Gauntlet (รวมพรมแดง): กระโดด — ใช้ร่วมกับ Space/W/↑ และปุ่ม UI */ + function tryRequestGauntletJumpPlay() { + if (!mapData || !isGauntlet() || isChatFocused()) return false; + if (isGauntletCrownPregameBlockingPlay()) return false; + if (isGauntletCrownHeistMapPlay() && me.gauntletEliminated) return false; + const now = Date.now(); + if (now - lastGauntletJumpKey < 200) return false; + lastGauntletJumpKey = now; + socket.emit('gauntlet-jump'); + return true; + } + + function syncGauntletCrownJumpButton() { + const btn = document.getElementById('gauntlet-crown-jump-btn'); + if (!btn) return; + const show = !!(mapData && isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live' && !me.gauntletEliminated && gauntletCrownRunwayAvatarRunAllowedPlay()); + if (!show) { + btn.classList.add('is-hidden'); + btn.setAttribute('aria-hidden', 'true'); + btn.style.pointerEvents = 'none'; + return; + } + btn.classList.remove('is-hidden'); + btn.setAttribute('aria-hidden', 'false'); + btn.style.pointerEvents = ''; + } + + function syncStackTowerDropButtonPlay() { + const btn = document.getElementById('stack-tower-drop-btn'); + if (!btn) return; + if (!mapData || !isStack() || !stackMini) { + btn.classList.add('is-hidden'); + btn.setAttribute('aria-hidden', 'true'); + btn.style.pointerEvents = 'none'; + btn.classList.remove('stack-tower-drop-btn--active'); + return; + } + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase !== 'live') { + btn.classList.add('is-hidden'); + btn.setAttribute('aria-hidden', 'true'); + btn.style.pointerEvents = 'none'; + btn.classList.remove('stack-tower-drop-btn--active'); + return; + } + btn.classList.remove('is-hidden'); + btn.setAttribute('aria-hidden', 'false'); + const ok = stackHumanDropGateOkPlay(); + btn.classList.toggle('stack-tower-drop-btn--active', ok); + btn.style.pointerEvents = ok ? '' : 'none'; + } + + function resetQuizCarryPlayState() { + cancelQuizCarrySessionCompleteResultToSummary(); + const qcmo = document.getElementById('quiz-carry-mission-overlay'); + if (qcmo) qcmo.classList.add('is-hidden'); + quizCarryPregameActive = false; + hideQuizCarryPregameOverlay(); + quizCarryPool = []; + quizCarryCurrent = null; + quizCarryEmbedPendingQuestion = null; + quizCarryEmbedCountdownStartAt = 0; + quizCarryEmbedCountdownEndAt = 0; + quizCarryEmbedPreOptionCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownEndAt = 0; + quizCarryOptionRevealAt = 0; + quizCarryAnswerCloseAt = 0; + quizCarryAnswerTimeupAwaitNext = false; + quizCarryRoundsCompleted = 0; + quizCarrySessionEnded = false; + hideQuizCarryEmbedCountdownOverlay(); + hideQuizCarryTimeupOnDeskLayer(); + hideQuizCarryResultEndLayer(); + me.quizCarryHeld = null; + others.forEach((o) => { + if (!o) return; + o.quizCarryHeld = null; + o.botPath = []; + }); + } + + /** รวม carry จากดิสก์ — ใช้เฉพาะ GET ที่ Node รองรับ (path เดียวกับ server.js) ไม่เรียก .php เพื่อกัน 404 บน nginx/php-fpm */ + async function mergeQuizCarrySettingsFromDisk(s) { + const out = s && typeof s === 'object' ? s : {}; + try { + const rd = await fetch(BASE + '/api/quiz-carry-from-disk?_=' + Date.now(), { cache: 'no-store' }); + if (rd.ok) { + const disk = await rd.json(); + if (disk && typeof disk === 'object') { + if (disk.carryMapPanelTheme && typeof disk.carryMapPanelTheme === 'object') { + out.carryMapPanelTheme = disk.carryMapPanelTheme; + } + if (disk.quizMapPanelTheme && typeof disk.quizMapPanelTheme === 'object') { + out.quizMapPanelTheme = disk.quizMapPanelTheme; + } + if (disk.carryEmbedCountdownTheme && typeof disk.carryEmbedCountdownTheme === 'object') { + out.carryEmbedCountdownTheme = disk.carryEmbedCountdownTheme; + } + if (Array.isArray(disk.carryChoicePlaqueThemes) && disk.carryChoicePlaqueThemes.length) { + out.carryChoicePlaqueThemes = disk.carryChoicePlaqueThemes; + } else if (disk.carryChoicePlaqueTheme && typeof disk.carryChoicePlaqueTheme === 'object') { + out.carryChoicePlaqueTheme = disk.carryChoicePlaqueTheme; + } + if (disk.carryReadMs != null) out.carryReadMs = disk.carryReadMs; + if (disk.carryAnswerMs != null) out.carryAnswerMs = disk.carryAnswerMs; + if (disk.carrySessionLength != null) out.carrySessionLength = disk.carrySessionLength; + if (disk.carryWalkSpeedMultForMapId != null) { + out.carryWalkSpeedMultForMapId = String(disk.carryWalkSpeedMultForMapId).trim(); + } + if (disk.carryWalkSpeedMult != null) { + const wm = Number(disk.carryWalkSpeedMult); + if (Number.isFinite(wm)) out.carryWalkSpeedMult = wm; + } + const diskPlaqueScale = Number(disk.carryChoicePlaqueMapScale); + if (Number.isFinite(diskPlaqueScale)) { + out.carryChoicePlaqueMapScale = Math.max(0.85, Math.min(2.5, diskPlaqueScale)); + } + if (Array.isArray(disk.carryQuestions) && disk.carryQuestions.length > 0) { + out.carryQuestions = disk.carryQuestions; + } + } + } + } catch (e) { /* ignore */ } + return out; + } + + async function loadQuizCarryPoolAndStart() { + cancelQuizCarrySessionCompleteResultToSummary(); + quizCarryEmbedPreOptionCountdownStartAt = 0; + quizCarryEmbedPreOptionCountdownEndAt = 0; + quizCarryRoundsCompleted = 0; + quizCarrySessionEnded = false; + hideQuizCarryTimeupOnDeskLayer(); + hideQuizCarryResultEndLayer(); + quizCarrySessionLength = 0; + let pool = []; + let s = {}; + const snap = quizCarryJoinSettingsSnap && typeof quizCarryJoinSettingsSnap === 'object' ? quizCarryJoinSettingsSnap : null; + try { + const r = await fetch(BASE + '/api/quiz-settings?_=' + Date.now(), { cache: 'no-store' }); + if (r.ok) { + const raw = await r.text(); + const trimmed = (raw || '').trim(); + if (trimmed.startsWith('{') && trimmed.endsWith('}')) { + const parsed = JSON.parse(trimmed); + if (parsed && typeof parsed === 'object') s = parsed; + } + } + } catch (e) { /* Node/HTML error — ต่อจาก merge ดิสก์ */ } + try { + s = await mergeQuizCarrySettingsFromDisk(s); + } catch (e) { /* ignore */ } + if (snap) { + if (isDetectiveMinigamePlay()) { + if (snap.carryReadMs != null) s.carryReadMs = snap.carryReadMs; + if (snap.carryAnswerMs != null) s.carryAnswerMs = snap.carryAnswerMs; + s.carrySessionLength = 1; + } else { + if (s.carryReadMs == null && snap.carryReadMs != null) s.carryReadMs = snap.carryReadMs; + if (s.carryAnswerMs == null && snap.carryAnswerMs != null) s.carryAnswerMs = snap.carryAnswerMs; + if (s.carrySessionLength == null && snap.carrySessionLength != null) s.carrySessionLength = snap.carrySessionLength; + } + } + if (snap && snap.carryMapPanelTheme && typeof snap.carryMapPanelTheme === 'object') { + s.carryMapPanelTheme = snap.carryMapPanelTheme; + } + if (snap && snap.carryEmbedCountdownTheme && typeof snap.carryEmbedCountdownTheme === 'object') { + s.carryEmbedCountdownTheme = snap.carryEmbedCountdownTheme; + } + /* ไม่ทับ carryChoicePlaqueThemes จาก snap ถ้ามีแล้ว — snapshot ตอน join อาจเก่ากว่าไฟล์ที่เพิ่งบันทึกใน Admin (รูปป้ายหาย) */ + if (snap && (!Array.isArray(s.carryChoicePlaqueThemes) || !s.carryChoicePlaqueThemes.length)) { + if (Array.isArray(snap.carryChoicePlaqueThemes) && snap.carryChoicePlaqueThemes.length) { + s.carryChoicePlaqueThemes = snap.carryChoicePlaqueThemes; + } else if (snap.carryChoicePlaqueTheme && typeof snap.carryChoicePlaqueTheme === 'object') { + s.carryChoicePlaqueTheme = snap.carryChoicePlaqueTheme; + } + } + if (snap && (s.carryChoicePlaqueMapScale == null || !Number.isFinite(Number(s.carryChoicePlaqueMapScale)))) { + const sm = Number(snap.carryChoicePlaqueMapScale); + if (Number.isFinite(sm)) s.carryChoicePlaqueMapScale = sm; + } + if (snap) { + if ((s.carryWalkSpeedMultForMapId == null || String(s.carryWalkSpeedMultForMapId).trim() === '') && snap.carryWalkSpeedMultForMapId != null) { + s.carryWalkSpeedMultForMapId = snap.carryWalkSpeedMultForMapId; + } + if (s.carryWalkSpeedMult == null && snap.carryWalkSpeedMult != null) s.carryWalkSpeedMult = snap.carryWalkSpeedMult; + } + applyQuizCarryWalkSpeedFromSettingsObj(s); + { + const sc = Number(s.carryChoicePlaqueMapScale); + quizCarryPlaqueMapScale = Number.isFinite(sc) ? Math.max(0.85, Math.min(2.5, sc)) : 1.25; + } + try { + quizCarryCarryTimingMs = { + carryReadMs: clampPreviewMs(s.carryReadMs, 3000, 0, 120000), + carryAnswerMs: clampPreviewMs(s.carryAnswerMs, 5000, 1000, 300000), + }; + quizCarrySessionLength = clampCarrySessionLen(s.carrySessionLength, 0); + setQuizCarryMapPanelThemeFromApi(s); + setQuizCarryEmbedCountdownThemeFromApi(s); + setQuizCarryChoicePlaqueThemeFromApi(s); + for (let ti = 0; ti < QUIZ_CARRY_MAX_OPTION_SLOTS; ti++) { + const tUrl = sanitizeQuizCarryImageUrlClient(getEffectiveCarryChoicePlaqueThemeForChoice(ti).plaqueImageUrl); + if (!tUrl || quizCarryChoiceImageCache.has(tUrl)) continue; + const pim = new Image(); + quizCarryApplyImageCrossOrigin(pim, tUrl); + pim.onload = () => { try { draw(); } catch (e) { /* ignore */ } }; + quizCarryChoiceImageCache.set(tUrl, pim); + try { + pim.src = tUrl; + } catch (e) { /* ignore */ } + } + const carryOnly = s && Array.isArray(s.carryQuestions) && s.carryQuestions.length > 0; + const source = carryOnly ? s.carryQuestions : (s && Array.isArray(s.questions) ? s.questions : []); + for (let i = 0; i < source.length; i++) { + const n = normalizeQuizCarryQuestionFromAny(source[i]); + if (n) pool.push(n); + } + } catch (e) { /* map only */ } + if (!pool.length && mapData) pool = buildQuizCarryPoolFromMap(mapData); + quizCarryPool = pool; + for (let pi = 0; pi < pool.length; pi++) preloadQuizCarryChoiceImages(pool[pi]); + playLiveQuizScores = {}; + if (myId != null) playLiveQuizScores[myId] = 0; + others.forEach((_, id) => { playLiveQuizScores[id] = 0; }); + if (!pool.length) { + playQuizText = 'ไม่มีคำถาม — ตั้งใน Admin → คำถามหลายตัวเลือก หรือใส่ในแมป (quizQuestions)'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = playQuizText; + try { + syncPlayQuizMapPanel(); + } catch (e) { /* ignore */ } + quizCarryJoinSettingsSnap = null; + return; + } + if (previewMode && editorEmbedReturn) { + beginQuizCarryEmbedPregame(); + } else if (isDetectiveMinigamePlay()) { + quizCarryPickNextQuestion(); + } else { + quizCarryPickNextQuestion(); + } + const nowJoin = Date.now(); + if (!(previewMode && editorEmbedReturn && quizCarryEmbedCountdownEndAt > nowJoin) + && !(previewMode && editorEmbedReturn && quizCarryEmbedPreOptionCountdownEndAt > nowJoin)) { + quizCarryRestoreEmbedPhaseLabel(); + } + try { + syncPlayQuizMapPanel(); + } catch (e) { /* ignore */ } + quizCarryJoinSettingsSnap = null; + } + + function setupPlayQuizCarryUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) { + if (isDetectiveMinigamePlay() || (previewMode && editorEmbedReturn)) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } else { + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + } + const tEl = document.getElementById('quiz-game-timer'); + if (tEl) tEl.textContent = ''; + const leg = document.getElementById('quiz-play-legend'); + if (leg) { + leg.textContent = isDetectiveMinigamePlay() + ? '' + : (quizCarryMapHasAnswerInteractive(mapData) + ? 'แต่ละตัวเลือกมีคนถือได้คนเดียว — ส่งคำตอบที่โซน interactive (เขียว) · โซนกลางม่วงเป็นกำแพง · กด F หรือปุ่ม GRAB มุมขวาล่าง' + : 'แต่ละตัวเลือกมีคนถือได้คนเดียว — โซนกลางเป็นกำแพง · ยืนชิดขอบแล้วกด F หรือปุ่ม GRAB ส่งเมื่อถือป้าย'); + } + resetQuizCarryPlayState(); + if (previewMode) { + loadQuizCarryPoolAndStart(); + } else { + playQuizText = isDetectiveMinigamePlay() ? '' : 'เข้าห้องแล้ว — คำถามจากแมป (เล่นจริงแนะนำซิงก์ผ่านโฮสต์ในอนาคต)'; + loadQuizCarryPoolAndStart(); + } + renderPlayQuizScoreboard(playLiveQuizScores); + syncQuizCarryGrabButton(); + } + + function pickRandomTileForQuizCarryOption(md, optionIndex, o) { + const w = md.width || 20, h = md.height || 15; + const want = optionIndex + 1; + const pool = []; + for (let y = 0; y < h; y++) { + const row = md.quizCarryOptionArea && md.quizCarryOptionArea[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (Number(row[x]) === want && spawnTileWalkablePlay(md, x, y)) { + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, o.x, o.y, o)) pool.push({ x, y }); + } + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function pickRandomTileInQuizCarryHub(md, o) { + const w = md.width || 20, h = md.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = md.quizCarryHubArea && md.quizCarryHubArea[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1 && spawnTileWalkablePlay(md, x, y)) { + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, o.x, o.y, o)) pool.push({ x, y }); + } + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function pickRandomTileInQuizCarryInteractive(md, o) { + const w = md.width || 20, h = md.height || 15; + const pool = []; + for (let y = 0; y < h; y++) { + const row = md.interactive && md.interactive[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1 && spawnTileWalkablePlay(md, x, y)) { + if (canWalkLikeLobbyForBot(x + 0.5, y + 0.5, o.x, o.y, o)) pool.push({ x, y }); + } + } + } + if (!pool.length) return null; + return pool[Math.floor(Math.random() * pool.length)]; + } + + function pickRandomTileForQuizCarrySubmit(md, o) { + if (quizCarryMapHasAnswerInteractive(md)) { + const p = pickRandomTileInQuizCarryInteractive(md, o); + if (p) return p; + } + return pickRandomTileInQuizCarryHub(md, o); + } + + /** ช่วงไม่มีข้อ / รอตัวเลือก — ให้บอทเดินเล่นในแมปเหมือน preview ทั่วไป (กันยืนนิ่งทั้งเกม) */ + function quizCarryPreviewBotLobbyWanderStep(o, w, h) { + const now = Date.now(); + o.botIsWalking = false; + if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + if (typeof o.botWanderNextTurn !== 'number') o.botWanderNextTurn = now + 600; + if (now >= o.botWanderNextTurn) { + o.botWanderNextTurn = now + 650 + Math.floor(Math.random() * 2200); + if (Math.random() < 0.55) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + } + const accX = o.botWanderDx; + const accY = o.botWanderDy; + if (Math.abs(accY) > Math.abs(accX)) o.direction = accY > 0 ? 'down' : 'up'; + else if (accX !== 0) o.direction = accX > 0 ? 'right' : 'left'; + const step = MOVE_SPEED * quizCarryWalkSpeedMultActive; + const nx = o.x + accX * step; + const ny = o.y + accY * step; + const ox = o.x; + const oy = o.y; + if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) { + o.x = nx; + o.y = ny; + } else { + if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) { + o.x = nx; + } else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) { + o.y = ny; + } else { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600); + } + } + if (!Number.isFinite(o.x)) o.x = 0.5; + if (!Number.isFinite(o.y)) o.y = 0.5; + clampPlayEntityFootprintToMap(o, mapData); + if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) o.botIsWalking = true; + } + + function stepQuizCarryPreviewBots() { + if (quizCarrySessionEnded) return; + if (quizCarryPregameActive) return; + if (!playBotsEnabled() || !isQuizCarry() || !mapData) return; + const w = mapData.width || 20, h = mapData.height || 15; + const nowPf = performance.now(); + const pathfindMinGap = editorEmbedReturn ? 280 : 140; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + if (quizCarryFootprintOverlapsHub(o.x, o.y)) { + const sn = snapPositionOutOfQuizCarryHubIfNeeded(o.x, o.y); + if (Math.abs(sn.x - o.x) > 0.02 || Math.abs(sn.y - o.y) > 0.02) { + o.x = sn.x; + o.y = sn.y; + o.botPath = []; + o.botPathStuckTicks = 0; + o.botQuizCarryPathfindAfter = 0; + } + } + if (!o.botPath || o.botPath.length === 0) { + tryQuizCarryInteractionForPlayer(id, o.x, o.y, { silent: true }); + } + /* ช่วงนับถอยหลัง / เปลี่ยนข้อ — ยังให้เดินได้ */ + if (!quizCarryCurrent) { + quizCarryPreviewBotLobbyWanderStep(o, w, h); + return; + } + const tier = o.botTier || 'avg'; + const pCorrect = tier === 'sharp' ? 0.9 : tier === 'weak' ? 0.35 : 0.65; + const rawWant = Math.random() < pCorrect ? quizCarryCurrent.correctIndex + : Math.floor(Math.random() * Math.max(2, (quizCarryCurrent.choices || []).length)); + const wantIdx = pickQuizCarryTargetOptionIndexAvoidingTaken(rawWant); + if (!o.botPath || !o.botPath.length) { + if (typeof o.botQuizCarryPathfindAfter !== 'number') o.botQuizCarryPathfindAfter = 0; + if (nowPf < o.botQuizCarryPathfindAfter) { + quizCarryPreviewBotLobbyWanderStep(o, w, h); + return; + } + const jitter = (String(id).length * 31 + ((o.x | 0) ^ (o.y | 0)) * 7) % 160; + if (quizCarryOptionsPickableNow()) { + if (o.quizCarryHeld == null && wantIdx != null) { + const dest = pickRandomTileForQuizCarryOption(mapData, wantIdx, o); + if (dest) { + const path = pathfindPlayForBot(o.x, o.y, dest.x + 0.5, dest.y + 0.5, o); + if (path && path.length > 1) o.botPath = path.slice(1); + } + } else { + const sub = pickRandomTileForQuizCarrySubmit(mapData, o); + if (sub) { + const path = pathfindPlayForBot(o.x, o.y, sub.x + 0.5, sub.y + 0.5, o); + if (path && path.length > 1) o.botPath = path.slice(1); + } + } + o.botQuizCarryPathfindAfter = nowPf + pathfindMinGap + jitter; + } else { + o.botQuizCarryPathfindAfter = nowPf + 120; + quizCarryPreviewBotLobbyWanderStep(o, w, h); + } + } else { + stepPreviewBotAlongPath(o, w, h); + } + clampPlayEntityFootprintToMap(o, mapData); + }); + separateClumpedPreviewBots(); + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const ob = others.get(bid); + if (ob) clampPlayEntityFootprintToMap(ob, mapData); + }); + } + + function getStackAreaBoundsPlay(area, w, h) { + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + let any = false; + for (let y = 0; y < h; y++) { + const row = area && area[y]; + if (!row) continue; + for (let x = 0; x < w; x++) { + if (row[x] === 1) { + any = true; + minX = Math.min(minX, x); + maxX = Math.max(maxX, x); + minY = Math.min(minY, y); + maxY = Math.max(maxY, y); + } + } + } + if (!any) return null; + return { + minX, maxX, minY, maxY, + cx: (minX + maxX + 1) / 2, + cy: (minY + maxY + 1) / 2, + }; + } + + /** ปิดการตามความสูงหอจาก JSON เสมอ — เซิร์ฟเคยบังคับ enabled:true ใน policy ทำให้ boost เลื่อนโลก */ + function getStackTowerCameraFollowPlayConfig() { + return { enabled: false, pxPerLayer: 12, maxPx: 260 }; + } + + /** ยกจุดยึดเชือกขึ้นเมื่อหอสูง (y ลดลง = สูงขึ้นในโลก) — เฉพาะ mnn93hpi + เปิดกล้องตามหอ */ + function getStackTowerSwingLiftWorldPx(layerCount, layerWorldHPx) { + if (!isStackTowerMissionUiMapPlay() || !getStackTowerCameraFollowPlayConfig().enabled) return 0; + const n = Math.floor(Number(layerCount) || 0); + if (n < STACK_TOWER_SWING_LIFT_FROM_LAYER) return 0; + const lh = Math.max(10, Number(layerWorldHPx) || Math.max(14, (tileSize || 32) * 0.3)); + const tiers = n - (STACK_TOWER_SWING_LIFT_FROM_LAYER - 1); + return Math.min(lh * 20, tiers * lh * 0.98); + } + + /** + * progress ≥เกณฑ์ Tower + ชั้นพอ: ไม่วาดชั้นล่าง (ข้าม draw) — ตำแหน่ง Y ยังใช้ชั้นจริง (floorY-(i+1)*h) + * เพื่อไม่ให้กองลอยหลุดจากฐานแมปเมื่อเทียบกริด + */ + function getStackTowerVisualHiddenLayerCountPlay() { + if (!isStackTowerMissionHudActivePlay() || !stackMini) return 0; + const p = Number(stackMini.progressPct) || 0; + if (p < STACK_TOWER_POST50_PROGRESS_THRESH) return 0; + const n = stackMini.layers ? stackMini.layers.length : 0; + if (n < 4) return 0; + return Math.floor(n / 2); + } + + /** + * world px ตามความสูงหอจริง (mnn93hpi) — คูณ z แล้วได้พิกเซลจอสำหรับเลื่อนแถบ BG + * ใช้ n × layerWorldH (ความสูงชั้นตามบล็อก) คล้มด้วย maxPx จากแมป + */ + function getStackTowerBgScrollHeightBoostPx() { + if (!isStackTowerMissionUiMapPlay() || !stackMini) return 0; + const cfg = getStackTowerCameraFollowPlayConfig(); + if (!cfg.enabled || cfg.pxPerLayer <= 0) return 0; + const lh = Math.max(10, Number(stackMini.layerWorldH) || Math.max(14, (tileSize || 32) * 0.31)); + const n = stackMini.layers ? stackMini.layers.length : 0; + let layerCount = n; + if (stackFall) { + const dur = Math.max(1, stackFall.dur || 400); + const u = Math.min(1, Math.max(0, (performance.now() - stackFall.t0) / dur)); + layerCount = n + u * 0.9; + } + return Math.min(cfg.maxPx, layerCount * lh); + } + + /** กล้อง stack: จุดกลางจาก land / release ของแมป (Tower ไม่เลื่อน cy ตามกอง) */ + function getStackCameraCentersPx() { + const w = mapData.width || 20, h = mapData.height || 15; + const ts = tileSize; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); + const rel = getStackAreaBoundsPlay(mapData.stackReleaseArea, w, h); + let cx = (w / 2) * ts; + let cy = (h / 2) * ts; + if (land && rel) { + cx = land.cx * ts; + cy = ((land.cy + rel.cy) / 2) * ts; + } else if (land) { + cx = land.cx * ts; + cy = land.cy * ts; + } else if (rel) { + cx = rel.cx * ts; + cy = rel.cy * ts; + } + return { px: cx, py: cy }; + } + + /** ใช้ playStackBlockWidthTiles + แผนที่ — เรียกหลังโหลด /api/game-timing เมื่อหอว่าง */ + function reapplyStackMiniSizingFromGlobals() { + if (!mapData || !isStack() || !stackMini) return; + if (stackMini.layers && stackMini.layers.length > 0) return; + if (stackFall || stackMini.settling) return; + const wMap = mapData.width || 20; + const hMap = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, wMap, hMap); + const autoW = Math.min(3.2, Math.max(0.85, land ? (land.maxX - land.minX + 1) * 0.48 : 2)); + const iw = playStackBlockWidthTiles != null && Number.isFinite(playStackBlockWidthTiles) + ? Math.max(0.85, Math.min(3.2, playStackBlockWidthTiles)) + : autoW; + stackMini.initialWidthTiles = iw; + stackMini.widthTiles = iw; + markStackNextDropVisualDirty(); + } + + function resetStackMinigameState() { + stackMini = null; + stackTowerPost50AnimStartMs = null; + stackTowerHeartMinusFx = null; + if (!mapData || !isStack()) return; + const w = mapData.width || 20, h = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); + const rel = getStackAreaBoundsPlay(mapData.stackReleaseArea, w, h); + let towerCX = w * 0.5; + let swingAmp = 1.85; + let floorWorldY = (h - 1) * tileSize; + let swingWorldY = floorWorldY - tileSize * 1.35; + if (land) { + towerCX = land.cx; + floorWorldY = (land.maxY + 1) * tileSize; + const span = Math.max(1, land.maxX - land.minX + 1); + swingAmp = Math.min(span * 0.44, Math.max(0.6, span * 0.36)); + } + if (rel) { + swingWorldY = (rel.cy + 0.5) * tileSize; + const spanR = Math.max(1, rel.maxX - rel.minX + 1); + swingAmp = Math.max(swingAmp, Math.min(spanR * 0.42, 4.2)); + } + const autoW = Math.min(3.2, Math.max(0.85, land ? (land.maxX - land.minX + 1) * 0.48 : 2)); + const initW = playStackBlockWidthTiles != null && Number.isFinite(playStackBlockWidthTiles) + ? Math.max(0.85, Math.min(3.2, playStackBlockWidthTiles)) + : autoW; + stackFall = null; + const towerMission = isStackTowerMissionUiMapPlay(); + const towerMul = towerMission ? STACK_TOWER_BLOCK_WORLD_SCALE : 1; + const towerLayerStripH = Math.max(14, tileSize * 0.31) * towerMul; + const missMax = Math.max(1, Math.min(20, Math.floor(Number(playStackTeamMissesMax) || 3))); + stackMini = { + topCenterX: towerCX, + widthTiles: initW, + initialWidthTiles: initW, + craneWorldX: towerCX * tileSize, + swingAmp, + phase: Math.random() * Math.PI * 2, + phaseSpeed: playStackSwingHz, + floorWorldY, + swingWorldY, + layerWorldH: towerMission ? towerLayerStripH : Math.max(14, tileSize * 0.3), + blockStripH: towerMission ? towerLayerStripH : Math.max(16, tileSize * 0.32), + layers: [], + lives: 3, + teamMissesLeft: towerMission ? missMax : null, + progressPct: 0, + score: 0, + combo: 0, + lastDropAt: 0, + stackTiltRad: 0, + stackTiltVel: 0, + nextDropVisualDirty: true, + pendingDropSeat: 1, + pendingDropHeavy: false, + scorePopups: [], + perfectStackX2Fx: null, + }; + if (playBotsEnabled()) { + me.stackPreviewHumanPts = 0; + stackPreviewHudLog = []; + } + ensureStackTowerSlingImagePlay(); + if (towerMission || playBotsEnabled()) { + ensureStackTowerLifeHudImagesPlay(); + ensureStackTowerHeartMinusImgPlay(); + } + updateStackTowerSwingYForStripGapPlay(); + } + + function stackEaseDrop(t) { + t = Math.max(0, Math.min(1, t)); + return t * t; + } + + /** ตกช้า/นุ่มขึ้นเมื่อปล่อยเพี้ยนกลาง (คล้าย Tower of Babel — มีน้ำหนักก่อนนิ่ง) */ + function stackEaseDropPhys(t, sloppyN) { + t = Math.max(0, Math.min(1, t)); + if (sloppyN < 0.14) return t * t; + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; + } + + /** + * @param {object} hit + * @param {{ human?: boolean, botId?: string } | undefined} previewAttribution — โหมด preview: แยกคะแนนแสดงในแถบ (ตึกยังเป็นของร่วม) + */ + function applyStackHitFromDrop(hit, previewAttribution, layerVisualMeta) { + if (!stackMini || !hit) return; + const towerLive = isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'live'; + if (hit.miss) { + stackMini.combo = 0; + stackMini.scorePopups = []; + stackMini.perfectStackX2Fx = null; + if (stackMini.layers && stackMini.layers.length) { + for (let gi = 0; gi < stackMini.layers.length; gi++) { + const lay = stackMini.layers[gi]; + if (lay && lay.perfectGlowUntil != null) delete lay.perfectGlowUntil; + } + } + if (towerLive && stackMini.teamMissesLeft != null) { + stackMini.teamMissesLeft = Math.max(0, stackMini.teamMissesLeft - 1); + } else { + stackMini.lives = Math.max(0, stackMini.lives - 1); + if (stackMini.lives <= 0) { + stackMini.score = Math.max(0, stackMini.score - 2); + stackMini.lives = 3; + stackMini.layers = []; + stackMini.scorePopups = []; + stackMini.perfectStackX2Fx = null; + stackMini.widthTiles = stackMini.initialWidthTiles; + const lb = getStackAreaBoundsPlay(mapData.stackLandArea, mapData.width || 20, mapData.height || 15); + stackMini.topCenterX = lb ? lb.cx : (mapData.width || 20) * 0.5; + stackMini.combo = 0; + stackMini.stackTiltRad = 0; + stackMini.stackTiltVel = 0; + } + } + if (towerLive) triggerStackTowerHeartMinusFxPlay(); + } else { + const seatLay = layerVisualMeta && layerVisualMeta.seat != null ? layerVisualMeta.seat : getStackDropActorSeat(); + const heavyLay = !!(layerVisualMeta && layerVisualMeta.heavy); + const placedW = hit.placedW != null && Number.isFinite(hit.placedW) + ? Math.max(0.85, Math.min(3.2, hit.placedW)) + : stackMini.initialWidthTiles; + stackMini.layers.push({ w: placedW, cx: hit.placedCx, seat: seatLay, heavy: heavyLay }); + const nAfter = stackMini.layers.length; + maybeQueueStackTowerBgScrollStepPlay(nAfter); + const perfectFull = + towerLive && + hit.perfect && + !hit.miss && + nAfter >= 2 && + hit.supportRatio != null && + hit.supportRatio >= STACK_TOWER_PERFECT_SUPPORT_MIN; + if (perfectFull) { + const gUntil = performance.now() + STACK_TOWER_PERFECT_GLOW_MS; + stackMini.layers[nAfter - 1].perfectGlowUntil = gUntil; + stackMini.layers[nAfter - 2].perfectGlowUntil = gUntil; + } + if (towerLive && (perfectFull || hit.pts >= 20)) { + ensureStackTowerPerfectFxImagesPlay(); + stackMini.perfectStackX2Fx = { + until: performance.now() + STACK_TOWER_PERFECT_X2_MS, + topLayerIndex: nAfter - 1, + }; + } else if (towerLive) { + stackMini.perfectStackX2Fx = null; + } + stackMini.score += hit.pts; + if (towerLive && Number.isFinite(Number(hit.progressDelta)) && (hit.progressDelta || 0) > 0) { + let np = Number(stackMini.progressPct) + Number(hit.progressDelta); + np = Math.min(100, Math.max(0, np)); + if (np > 99.999) np = 100; + stackMini.progressPct = np; + markStackNextDropVisualDirty(); + } + if (playBotsEnabled() && previewAttribution) { + if (previewAttribution.human) { + me.stackPreviewHumanPts = (me.stackPreviewHumanPts || 0) + hit.pts; + } else if (previewAttribution.botId) { + const ob = others.get(previewAttribution.botId); + if (ob) ob.stackBotScore = (ob.stackBotScore || 0) + hit.pts; + } + } + stackMini.combo = hit.perfect ? stackMini.combo + 1 : 0; + stackMini.topCenterX = hit.placedCx; + if (towerLive && !hit.miss && hit.pts > 0) { + const pNow = Number(stackMini.progressPct); + const prog100 = Number.isFinite(pNow) && pNow >= 99.5; + const kind20 = hit.pts >= 20 || prog100; + if (!stackMini.scorePopups) stackMini.scorePopups = []; + const until = performance.now() + STACK_TOWER_SCORE_POPUP_MS; + stackMini.scorePopups.push({ until, layerIndex: nAfter - 1, kind: kind20 ? '20' : '10' }); + if (kind20 && perfectFull && nAfter >= 2) { + stackMini.scorePopups.push({ until, layerIndex: nAfter - 2, kind: '20' }); + } + while (stackMini.scorePopups.length > 12) stackMini.scorePopups.shift(); + } + } + } + + /** + * Editor embed เท่านั้น: ปรับ `stackMini.phase` ให้ sin·swingAmp ใกล้จุดรองรับก่อนปล่อย — บอทฉลาด/กลาง/พลาดบ่อย ต่างค่า aim error + */ + function previewBotAlignStackSwingPhasePlay(m, botPeer) { + if (!playBotsEnabled() || !m || !mapData) return; + const wMap = mapData.width || 20; + const hMap = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, wMap, hMap); + const fallWRaw = m.widthTiles != null && Number.isFinite(m.widthTiles) ? m.widthTiles : m.initialWidthTiles; + const fallW = Math.max(0.85, Math.min(3.2, fallWRaw)); + let supportCx; + let supportW; + if (m.layers.length === 0) { + supportCx = land ? land.cx : wMap * 0.5; + supportW = land ? (land.maxX - land.minX + 1) : Math.max(8, fallW * 2.2); + } else { + const sup = m.layers[m.layers.length - 1]; + supportCx = sup.cx; + const swR = sup.w != null && Number.isFinite(sup.w) ? sup.w : m.initialWidthTiles; + supportW = Math.max(0.85, Math.min(3.2, swR)); + } + const tier = botPeer && botPeer.botTier === 'sharp' ? 'sharp' : botPeer && botPeer.botTier === 'weak' ? 'weak' : 'avg'; + const layerTight = 1 / Math.sqrt(1 + (m.layers.length || 0) * 1.05); + const errMax = tier === 'sharp' ? 0.016 : tier === 'avg' ? 0.038 : 0.17; + const tri = Math.random() + Math.random() - 1; + let errTiles = tri * errMax * layerTight; + if (m.layers.length >= 2) { + const L = m.layers.length; + const step = m.layers[L - 1].cx - m.layers[L - 2].cx; + errTiles -= step * 0.28; + } + let targetCx = supportCx + errTiles; + if (land && m.layers.length > 0) { + const pull = (land.cx - supportCx) * 0.1; + const cap = tier === 'sharp' ? 0.09 : tier === 'avg' ? 0.12 : 0.06; + targetCx += Math.max(-cap, Math.min(cap, pull)); + } + const rawWant = targetCx - m.topCenterX; + const amp = Math.max(1e-5, m.swingAmp); + const s = Math.max(-1, Math.min(1, rawWant / amp)); + const phi0 = Math.asin(s); + const phi1 = Math.PI - phi0; + const twoPi = Math.PI * 2; + function nearestPhase(anchor, p) { + let best = p; + let bestD = Infinity; + for (let k = -5; k <= 5; k++) { + const cand = p + k * twoPi; + const d = Math.abs(cand - anchor); + if (d < bestD) { + bestD = d; + best = cand; + } + } + return best; + } + const n0 = nearestPhase(m.phase, phi0); + const n1 = nearestPhase(m.phase, phi1); + m.phase = Math.abs(n0 - m.phase) <= Math.abs(n1 - m.phase) ? n0 : n1; + } + + /** + * ชั้นรองรับใช้ความกว้างจริงต่อชั้น (L.w) · บล็อกที่กำลังตกใช้ m.widthTiles (ปกติ vs heavy กว้างต่างกัน) + * พลาดเมื่อทับแท่นไม่พอ หรือจุดกลางหลุดขอบฐาน (STACK_MAX_*) + */ + function computeStackDropResult(m) { + const raw = m.swingAmp * Math.sin(m.phase); + const wMap = mapData.width || 20; + const hMap = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, wMap, hMap); + const fallWRaw = m.widthTiles != null && Number.isFinite(m.widthTiles) ? m.widthTiles : m.initialWidthTiles; + const fallW = Math.max(0.85, Math.min(3.2, fallWRaw)); + const wMul = getStackTowerBlockWorldScalePlay(); + const fallWEff = fallW * wMul; + let supportCx; + let supportW; + if (m.layers.length === 0) { + supportCx = land ? land.cx : wMap * 0.5; + supportW = land ? (land.maxX - land.minX + 1) : Math.max(8, fallW * 2.2); + } else { + const sup = m.layers[m.layers.length - 1]; + supportCx = sup.cx; + const swR = sup.w != null && Number.isFinite(sup.w) ? sup.w : m.initialWidthTiles; + supportW = Math.max(0.85, Math.min(3.2, swR)); + } + const c1 = m.topCenterX + raw; + const half1 = fallWEff * 0.5; + const half0 = (m.layers.length === 0 ? supportW : supportW * wMul) * 0.5; + const left = Math.max(supportCx - half0, c1 - half1); + const right = Math.min(supportCx + half0, c1 + half1); + const overlap = right - left; + const missThreshold = + m.layers.length === 0 + ? Math.max(STACK_OVERLAP_MISS_MIN_ON_LAND, fallWEff * STACK_OVERLAP_MISS_FRAC_ON_LAND) + : Math.max(STACK_OVERLAP_MISS_MIN_ON_LAYER, fallWEff * STACK_OVERLAP_MISS_FRAC_ON_LAYER); + const overlapMiss = overlap < missThreshold; + const placedCx = c1; + let driftMiss = false; + if (!overlapMiss && m.layers.length > 0) { + const anchorCx = land ? land.cx : wMap * 0.5; + let maxDriftTiles; + if (land) { + const landSpan = Math.max(1, land.maxX - land.minX + 1); + maxDriftTiles = landSpan * STACK_MAX_CENTER_DRIFT_FRAC_OF_LAND; + } else { + maxDriftTiles = Math.max(0.85, fallW * STACK_MAX_CENTER_DRIFT_NO_LAND_MULT); + } + if (Math.abs(placedCx - anchorCx) > maxDriftTiles) driftMiss = true; + } + const miss = overlapMiss || driftMiss; + const delta = c1 - supportCx; + const span = supportW * 0.5 + fallW * 0.5; + const offN = Math.abs(delta) / Math.max(0.01, span); + const perfect = !miss && offN < 0.055; + const comboBefore = m.combo || 0; + let pts; + let progressDelta = 0; + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'live') { + const mult = !miss && perfect && comboBefore >= 1 ? 2 : 1; + const progPerLayer = 100 / stackTowerProgressBlocksPlay(); + pts = miss ? 0 : (10 * mult); + progressDelta = miss ? 0 : (progPerLayer * mult); + } else { + const bonus = perfect ? Math.min(4, comboBefore) : 0; + pts = perfect ? (2 + bonus) : (miss ? 0 : 1); + } + const supportRatio = fallWEff > 0 ? overlap / fallWEff : 0; + return { + miss, + perfect, + pts, + progressDelta, + landOffsetTiles: raw, + placedW: fallW, + placedCx, + overlap, + supportRatio, + delta, + }; + } + + /** + * เริ่มแอนิเมชันบล็อกร่วง (ผู้เล่น/บอทใช้ path เดียวกัน) + * @param {object} hit จาก computeStackDropResult + * @param {{ human?: boolean, botId?: string } | null} previewActor โหมด preview: ใครเป็นคนปล่อย (สำหรับคะแนน + เลื่อนตา) + * @returns {boolean} + */ + function startStackFallAnimation(hit, previewActor) { + if (!stackMini || !hit || stackFall || stackMini.settling) return false; + ensureStackNextDropVisual(); + const m = stackMini; + const actorSeat = m.pendingDropSeat != null ? m.pendingDropSeat : getStackDropActorSeat(); + const actorHeavy = !!m.pendingDropHeavy; + const tw = m.widthTiles * tileSize * getStackTowerBlockWorldScalePlay(); + const swingXW = (m.topCenterX + m.swingAmp * Math.sin(m.phase)) * tileSize; + const lh = m.layerWorldH || Math.max(14, tileSize * 0.3); + const swingLift = getStackTowerSwingLiftWorldPx(m.layers.length, lh); + const swingAttachY = m.swingWorldY - swingLift; + let y1 = m.floorWorldY - (m.layers.length + 1) * lh; + if (y1 <= swingAttachY) y1 = swingAttachY + lh * 0.55; + const landOff = hit.landOffsetTiles || 0; + const landCxWorld = (m.topCenterX + landOff) * tileSize; + const xLeft0 = swingXW - tw / 2; + const xLeft1 = landCxWorld - tw / 2; + const offN = Math.min(1, Math.abs(landOff) / Math.max(0.01, m.swingAmp)); + const dur = Math.max(400, Math.min(1280, 240 + (y1 - swingAttachY) * 0.92 + offN * 560)); + const tiltMax = hit.miss + ? 0.28 * (0.25 + offN) + : 0.16 * offN * (hit.perfect ? 0.1 : 1); + const isHumanPreview = !!(previewActor && previewActor.human); + const botIdPreview = previewActor && previewActor.botId ? previewActor.botId : null; + const cableTopYRel = isStackTowerMissionUiMapPlay() + ? (swingAttachY - tileSize * 6) + : Math.max(0, swingAttachY - tileSize * 6); + const craneXRel = m.craneWorldX != null ? m.craneWorldX : m.topCenterX * tileSize; + const releaseRopeAng = Math.atan2(swingAttachY - cableTopYRel, swingXW - craneXRel); + stackFall = { + t0: performance.now(), + dur, + xLeft0, + xLeft1, + tw, + y0: swingAttachY, + y1, + hit, + tiltMax, + sloppyN: offN, + previewHumanDrop: isHumanPreview, + previewBotId: botIdPreview, + releaseRopeAng, + releaseAttachWorldX: swingXW, + releaseAttachWorldY: swingAttachY, + actorSeat, + actorHeavy, + }; + return true; + } + + function stackHumanDropGateOkPlay() { + if (!stackMini || !isStack() || stackFall || stackMini.settling) return false; + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase !== 'live') return false; + if (playBotsEnabled() && mapData && mapData.gameType === 'stack' && + (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT)) { + rebuildStackPreviewTurnOrder(); + } + if (playBotsEnabled() && !isStackPreviewHumanTurn()) return false; + if (Date.now() - stackMini.lastDropAt < 480) return false; + return true; + } + + /** @returns {boolean} เริ่มแอนิเมชันร่วงแล้วหรือไม่ */ + function tryHumanStackDrop() { + if (!stackHumanDropGateOkPlay()) return false; + ensureStackNextDropVisual(); + const hit = computeStackDropResult(stackMini); + if (!startStackFallAnimation(hit, playBotsEnabled() ? { human: true } : null)) return false; + if (playBotsEnabled()) { + lastStackPreviewActorId = '__human__'; + lastStackPreviewActorUntil = Date.now() + 900; + } + return true; + } + + /** บอท preview: ปล่อยแบบมีแอนิเมชันร่วงเหมือนผู้เล่น — apply หลังแอนิเมชันจบ */ + function simulatePreviewBotStackDrop(botId, o) { + if (!stackMini || stackFall || stackMini.settling) return; + ensureStackNextDropVisual(); + previewBotAlignStackSwingPhasePlay(stackMini, o); + const hit = computeStackDropResult(stackMini); + if (!startStackFallAnimation(hit, playBotsEnabled() ? { botId } : null)) return; + o.stackBotGlowUntil = Date.now() + Math.max(900, 400 + (stackFall && stackFall.dur ? stackFall.dur : 800)); + lastStackPreviewActorId = botId; + lastStackPreviewActorUntil = Date.now() + Math.max(1000, 500 + (stackFall && stackFall.dur ? stackFall.dur : 800)); + } + + /** Stack preview: สลับตา — เฉพาะบอทที่ถึงตาถึงจะจำลองปล่อยหนึ่งครั้งแล้วเลื่อนตา */ + function stepPreviewStackTurnBased(nowMs) { + if (!playBotsEnabled() || !isStack() || !stackMini || stackFall || stackMini.settling) return; + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase !== 'live') return; + if (mapData && mapData.gameType === 'stack' && + (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT)) { + rebuildStackPreviewTurnOrder(); + } + const order = stackPreviewTurnOrder; + if (!order || order.length !== STACK_PREVIEW_TURN_COUNT) return; + const cur = order[stackPreviewTurnIndex]; + if (!cur || cur.kind !== 'bot') return; + if (!cur.botId) { + advanceStackPreviewTurn(); + return; + } + const o = others.get(cur.botId); + if (!o) { + advanceStackPreviewTurn(); + return; + } + if (stackPreviewBotThinkUntil === 0) { + const tier = o.botTier === 'sharp' ? 'sharp' : o.botTier === 'weak' ? 'weak' : 'avg'; + const thinkLo = tier === 'sharp' ? 160 : tier === 'avg' ? 260 : 340; + const thinkHi = tier === 'sharp' ? 340 : tier === 'avg' ? 520 : 620; + stackPreviewBotThinkUntil = nowMs + thinkLo + Math.random() * (thinkHi - thinkLo); + return; + } + if (nowMs < stackPreviewBotThinkUntil) return; + stackPreviewBotThinkUntil = 0; + simulatePreviewBotStackDrop(cur.botId, o); + } + + function stackTickFrame() { + stackTowerMissionMaybeEndPlay(); + const now = performance.now(); + const dt = Math.min(0.06, (now - lastStackTickMs) / 1000); + lastStackTickMs = now; + if (stackMini && stackMini.settling) { + const s = stackMini.settling; + const u = (now - s.t0) / s.dur; + if (u >= 1) { + const wasHuman = !!s.previewHumanDrop; + const settleBotId = s.previewBotId || null; + applyStackHitFromDrop({ miss: true, perfect: false, pts: 0, landOffsetTiles: 0 }, undefined); + if (playBotsEnabled() && (wasHuman || settleBotId)) { + stackPreviewLogStackDrop({ miss: true, perfect: false, pts: 0 }, { human: wasHuman, botId: settleBotId }); + } + stackMini.settling = null; + stackMini.lastDropAt = Date.now(); + markStackNextDropVisualDirty(); + if (playBotsEnabled() && (wasHuman || settleBotId)) advanceStackPreviewTurn(); + } + } else if (stackFall) { + const t = (now - stackFall.t0) / stackFall.dur; + if (t >= 1) { + const wasHuman = !!stackFall.previewHumanDrop; + const fallBotId = stackFall.previewBotId || null; + const hit = stackFall.hit; + const stableMin = STACK_STABLE_SUPPORT_RATIO_MIN; + if (hit.miss) { + applyStackHitFromDrop(hit); + if (playBotsEnabled() && (wasHuman || fallBotId)) { + stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId }); + } + stackMini.lastDropAt = Date.now(); + stackFall = null; + markStackNextDropVisualDirty(); + if (playBotsEnabled() && (wasHuman || fallBotId)) advanceStackPreviewTurn(); + } else if (hit.supportRatio != null && hit.supportRatio < stableMin) { + const stripH = stackMini.blockStripH || Math.max(16, tileSize * 0.32); + stackMini.settling = { + t0: performance.now(), + dur: Math.max(820, Math.min(2600, 720 + (1 - hit.supportRatio) * 3400)), + hit, + xLeft0: stackFall.xLeft1, + yTop0: stackFall.y1, + twWorld: stackFall.tw, + stripH, + delta: hit.delta || 0, + previewHumanDrop: wasHuman, + previewBotId: fallBotId, + actorSeat: stackFall.actorSeat, + actorHeavy: stackFall.actorHeavy, + }; + stackFall = null; + } else { + let previewAttr; + if (playBotsEnabled()) { + if (wasHuman) previewAttr = { human: true }; + else if (fallBotId) previewAttr = { botId: fallBotId }; + } + applyStackHitFromDrop(hit, previewAttr, { seat: stackFall.actorSeat, heavy: stackFall.actorHeavy }); + if (playBotsEnabled() && (wasHuman || fallBotId)) { + stackPreviewLogStackDrop(hit, { human: wasHuman, botId: fallBotId }); + } + stackMini.lastDropAt = Date.now(); + stackFall = null; + markStackNextDropVisualDirty(); + if (playBotsEnabled() && (wasHuman || fallBotId)) advanceStackPreviewTurn(); + } + } + } else if (stackMini) { + stackMini.phase += stackMini.phaseSpeed * dt * Math.PI * 2; + } + stepPreviewStackTurnBased(Date.now()); + } + + /** + * จุดบนจอที่เส้นก้าน→บล็อกตัดแนว y = -(ความสูงแคนวาส × STACK_TOWER_ROPE_TOP_ABOVE_CANVAS_FRAC) + * — ปลายเชือกอยู่เหนือขอบบน ~25% ของความสูงจอ (มองเห็นช่วงที่เข้ามาในจอ) + */ + function stackTowerRopeScreenTopAnchorPlay(slingBx, slingBy, tcx, tcy, canvasH) { + const ch = Math.max(80, Number(canvasH) || 1080); + const marginTop = -ch * STACK_TOWER_ROPE_TOP_ABOVE_CANVAS_FRAC; + const ddx = tcx - slingBx; + const ddy = tcy - slingBy; + if (Math.abs(ddy) < 1e-4) { + return { x: tcx, y: Math.min(marginTop, Math.min(tcy, slingBy)) }; + } + const vTop = (marginTop - slingBy) / ddy; + if (!Number.isFinite(vTop) || vTop < 0) { + return { x: tcx, y: tcy }; + } + const xTop = slingBx + vTop * ddx; + return { x: xTop, y: marginTop }; + } + + function applyStackPreviewSpawnLayout() { + if (!mapData || mapData.gameType !== 'stack') return; + const w = mapData.width || 20, h = mapData.height || 15; + const land = getStackAreaBoundsPlay(mapData.stackLandArea, w, h); + const cx = land ? land.cx : w / 2 - 0.5; + const cy = land ? land.cy : h / 2 - 0.5; + me.x = cx; + me.y = cy; + me.tx = cx; + me.ty = cy; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.x = cx + (Math.random() - 0.5) * 0.15; + o.y = cy + (Math.random() - 0.5) * 0.15; + o.tx = o.x; + o.ty = o.y; + o.stackBotScore = 0; + o.stackBotNextTry = Date.now() + 300 + Math.random() * 700; + }); + } + + function drawStackMinigame(ctx, worldToScreen, zoom) { + if (!stackMini) return; + const m = stackMini; + const towerHudLive = isStackTowerMissionHudActivePlay(); + const wMul = getStackTowerBlockWorldScalePlay(); + ensureStackNextDropVisual(); + const layerWorldH = m.layerWorldH || Math.max(14, tileSize * 0.3); + const stripH = m.blockStripH || Math.max(16, tileSize * 0.32); + const floorY = m.floorWorldY; + const swingLiftDraw = getStackTowerSwingLiftWorldPx(m.layers.length, layerWorldH); + const swingDrawY = m.swingWorldY - swingLiftDraw; + const hidLayers = towerHudLive ? getStackTowerVisualHiddenLayerCountPlay() : 0; + + const nLay = m.layers.length; + for (let i = hidLayers; i < nLay; i++) { + const L = m.layers[i]; + const lw = (L.w != null ? L.w : m.initialWidthTiles) * tileSize * wMul; + const cxW = L.cx * tileSize; + const yb = floorY - (i + 1) * layerWorldH; + const xl = cxW - lw / 2; + const [sx, sy] = worldToScreen(xl, yb); + const drawW = lw * zoom; + const drawH = layerWorldH * zoom; + const hue = (i * 47) % 360; + const seatL = L.seat != null ? L.seat : 1; + const heavyL = !!L.heavy; + if (towerHudLive && L.perfectGlowUntil != null) { + const ntGlow = performance.now(); + if (ntGlow < L.perfectGlowUntil) { + drawStackTowerPerfectYellowGlowBehindBlockPlay(ctx, sx, sy, drawW, drawH, ntGlow, L.perfectGlowUntil); + } + } + drawStackBlockSpriteOrHue(ctx, sx, sy, drawW, drawH, seatL, heavyL, hue, {}); + } + if (towerHudLive && m.perfectStackX2Fx) { + const fxX2 = m.perfectStackX2Fx; + const ntX = performance.now(); + if (ntX >= fxX2.until) { + m.perfectStackX2Fx = null; + } else { + const ti = fxX2.topLayerIndex; + if (ti >= hidLayers && ti >= 0 && ti < m.layers.length) { + const Ltop = m.layers[ti]; + const lwT = (Ltop.w != null ? Ltop.w : m.initialWidthTiles) * tileSize * wMul; + const cxWT = Ltop.cx * tileSize; + const ybT = floorY - (ti + 1) * layerWorldH; + const xlT = cxWT - lwT / 2; + const [sxT, syT] = worldToScreen(xlT, ybT); + const drawWT = lwT * zoom; + const drawHT = layerWorldH * zoom; + const fxImX = ensureStackTowerPerfectFxImagesPlay(); + if (fxImX.x2) { + drawStackTowerPerfectX2ClusterPlay(ctx, fxImX.light, fxImX.x2, sxT, syT, drawWT, drawHT, ntX, fxX2.until); + } + } + } + } + if (towerHudLive) { + drawStackTowerScorePopupsPlay(ctx, worldToScreen, zoom, m, layerWorldH, floorY); + } + if (m.settling) { + const s = m.settling; + const u = Math.min(1, (performance.now() - s.t0) / s.dur); + const ue = u * u; + const rotSign = s.delta >= 0 ? 1 : -1; + const rotRad = ue * 1.22 * rotSign; + const yFall = ue * ue * tileSize * 6.5; + const xSlide = rotSign * ue * tileSize * 0.62; + const cxW = s.xLeft0 + s.twWorld / 2 + xSlide; + const cyW = s.yTop0 + s.stripH * 0.5 + yFall; + const [cxs, cys] = worldToScreen(cxW, cyW); + const drawW = s.twWorld * zoom; + const drawH = s.stripH * zoom; + const hue = (m.layers.length * 47) % 360; + const seatS = s.actorSeat != null ? s.actorSeat : 1; + const heavyS = !!s.actorHeavy; + ctx.save(); + ctx.translate(cxs, cys); + ctx.rotate(rotRad); + drawStackBlockSpriteOrHue(ctx, -drawW / 2, -drawH / 2, drawW, drawH, seatS, heavyS, hue, {}); + ctx.restore(); + } else { + const twWorld = m.widthTiles * tileSize * wMul; + const drawStripPx = stripH * zoom; + const cableTopY = isStackTowerMissionUiMapPlay() + ? (swingDrawY - tileSize * 6) + : Math.max(0, swingDrawY - tileSize * 6); + const craneX = m.craneWorldX != null ? m.craneWorldX : m.topCenterX * tileSize; + let blockLeftWorld; + let blockTopWorld; + let fallTiltRad = 0; + if (stackFall) { + const tRaw = (performance.now() - stackFall.t0) / stackFall.dur; + const t = Math.min(1, tRaw); + const u = stackEaseDropPhys(t, stackFall.sloppyN); + blockLeftWorld = stackFall.xLeft0 + (stackFall.xLeft1 - stackFall.xLeft0) * u; + blockTopWorld = stackFall.y0 + (stackFall.y1 - stackFall.y0) * u; + fallTiltRad = stackFall.tiltMax * Math.sin(Math.PI * u); + } else { + const swingXW = (m.topCenterX + m.swingAmp * Math.sin(m.phase)) * tileSize; + blockLeftWorld = swingXW - twWorld / 2; + blockTopWorld = swingDrawY; + } + const cxMid = blockLeftWorld + twWorld / 2; + const [tcx, tcy] = worldToScreen(craneX, cableTopY); + const [bcx, bcy] = worldToScreen(cxMid, blockTopWorld); + let slingBx = bcx; + let slingBy = bcy; + if (stackFall && stackFall.releaseAttachWorldX != null) { + const attY = stackFall.releaseAttachWorldY; + const p = worldToScreen(stackFall.releaseAttachWorldX, attY); + slingBx = p[0]; + slingBy = p[1]; + } + let ropeSx0 = tcx; + let ropeSy0 = tcy; + let ropeSx1 = slingBx; + let ropeSy1 = slingBy; + if (isStackTowerMissionUiMapPlay()) { + const chCv = ctx.canvas && ctx.canvas.height ? ctx.canvas.height : STACK_TOWER_FIXED_RENDER_H; + const topA = stackTowerRopeScreenTopAnchorPlay(slingBx, slingBy, tcx, tcy, chCv); + ropeSx0 = topA.x; + ropeSy0 = topA.y; + ropeSx1 = slingBx; + ropeSy1 = slingBy; + const t1 = Math.max(0, Math.min(1, STACK_TOWER_ROPE_DRAW_T1)); + if (t1 < 1) { + ropeSx1 = ropeSx0 + (slingBx - ropeSx0) * t1; + ropeSy1 = ropeSy0 + (slingBy - ropeSy0) * t1; + } + } + drawStackSlingSegmentPlay(ctx, ropeSx0, ropeSy0, ropeSx1, ropeSy1, zoom); + const ropeAng = stackFall && stackFall.releaseRopeAng != null + ? stackFall.releaseRopeAng + : Math.atan2(bcy - ropeSy0, bcx - ropeSx0); + const swingTiltRad = ropeAng - Math.PI / 2; + const hitMissTint = stackFall && stackFall.hit && stackFall.hit.miss; + const seatDraw = stackFall && stackFall.actorSeat != null + ? stackFall.actorSeat + : (m.pendingDropSeat != null ? m.pendingDropSeat : getStackDropActorSeat()); + const heavyDraw = stackFall ? !!stackFall.actorHeavy : !!m.pendingDropHeavy; + const hueHint = (m.layers.length * 47) % 360; + ctx.save(); + ctx.translate(bcx, bcy); + ctx.rotate(fallTiltRad + swingTiltRad); + drawStackBlockSpriteOrHue(ctx, -twWorld * zoom / 2, 0, twWorld * zoom, drawStripPx, seatDraw, heavyDraw, hueHint, { missTint: !!hitMissTint, missOverlay: !!hitMissTint }); + ctx.lineWidth = 1; + ctx.restore(); + } + if (towerHudLive) { + drawStackTowerHeartMinusFxPlay(ctx, worldToScreen, zoom, m, layerWorldH, floorY, nLay); + } + } + + function applyGauntletTimingFromServer(payload) { + if (!payload || typeof payload !== 'object') return; + const tm = Number(payload.gauntletTickMs); + if (Number.isFinite(tm)) gauntletRuntimeTickMs = Math.max(80, Math.min(800, tm)); + const jt = Number(payload.gauntletJumpTicks); + if (Number.isFinite(jt)) gauntletRuntimeJumpTicks = Math.max(4, Math.min(40, jt)); + const tl = Number(payload.gauntletTimeLimitSec); + if (Number.isFinite(tl)) gauntletRuntimeTimeLimitSec = Math.max(0, Math.min(7200, tl)); + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletEndsAt')) { + if (payload.gauntletEndsAt == null) gauntletEndsAtMs = null; + else { + const ge = Number(payload.gauntletEndsAt); + gauntletEndsAtMs = Number.isFinite(ge) ? ge : null; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletCrownRunHeld')) { + gauntletCrownRunHeldRemote = !!payload.gauntletCrownRunHeld; + if (!gauntletCrownRunHeldRemote && usesCrownLobbyShellPlay()) { + gauntletCrownPregamePhase = 'live'; + trySnapGauntletCrownRunwayScrollToSpawnsPlay(); + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaneImageUrls') && Array.isArray(payload.gauntletLaneImageUrls)) { + gauntletLaneImageUrls = payload.gauntletLaneImageUrls + .filter((x) => typeof x === 'string') + .map((x) => normalizeGauntletAssetUrlForPlay(x)) + .filter((x) => x) + .slice(0, 24); + gauntletLaneImageUrls.forEach((x) => ensureGauntletAssetImage(x)); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserTopUrl')) { + gauntletLaserTopUrl = normalizeGauntletAssetUrlForPlay(typeof payload.gauntletLaserTopUrl === 'string' ? payload.gauntletLaserTopUrl : ''); + ensureGauntletAssetImage(gauntletLaserTopUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserBottomUrl')) { + gauntletLaserBottomUrl = normalizeGauntletAssetUrlForPlay(typeof payload.gauntletLaserBottomUrl === 'string' ? payload.gauntletLaserBottomUrl : ''); + ensureGauntletAssetImage(gauntletLaserBottomUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserLineUrl')) { + gauntletLaserLineUrl = normalizeGauntletAssetUrlForPlay(typeof payload.gauntletLaserLineUrl === 'string' ? payload.gauntletLaserLineUrl : ''); + ensureGauntletAssetImage(gauntletLaserLineUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserFillColor')) { + gauntletLaserFillColor = clampClientLaserColor(payload.gauntletLaserFillColor, gauntletLaserFillColor); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserStrokeColor')) { + gauntletLaserStrokeColor = clampClientLaserColor(payload.gauntletLaserStrokeColor, gauntletLaserStrokeColor); + } + if (Object.prototype.hasOwnProperty.call(payload, 'gauntletLaserLineWidthPx')) { + const lw = Number(payload.gauntletLaserLineWidthPx); + if (Number.isFinite(lw)) gauntletLaserLineWidthPx = Math.max(0, Math.min(24, Math.round(lw))); + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackSwingHz')) { + const sh = Number(payload.stackSwingHz); + if (Number.isFinite(sh)) playStackSwingHz = Math.max(0.08, Math.min(2.8, sh)); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackBlockWidthTiles')) { + const raw = payload.stackBlockWidthTiles; + if (raw == null || raw === '') playStackBlockWidthTiles = null; + else { + const nw = Number(raw); + playStackBlockWidthTiles = Number.isFinite(nw) && nw >= 0.85 + ? Math.max(0.85, Math.min(3.2, Math.round(nw * 100) / 100)) + : null; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackTowerMissionTimeSec')) { + const st = Number(payload.stackTowerMissionTimeSec); + if (Number.isFinite(st) && st > 0) { + playStackTowerMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(st))); + } else { + playStackTowerMissionTimeSec = 90; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackTeamMissesMax')) { + const sm = Number(payload.stackTeamMissesMax); + if (Number.isFinite(sm)) { + playStackTeamMissesMax = Math.max(1, Math.min(20, Math.floor(sm))); + } else { + playStackTeamMissesMax = 3; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackTowerProgressBlocks')) { + const pb = Number(payload.stackTowerProgressBlocks); + if (Number.isFinite(pb) && pb >= 1) { + playStackTowerProgressBlocks = Math.max(1, Math.min(500, Math.floor(pb))); + } else { + playStackTowerProgressBlocks = 50; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackBlockNormalImageUrls')) { + playStackBlockNormalUrls = normalizePlayStackSixUrlsFromTiming(payload.stackBlockNormalImageUrls); + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackBlockHeavyImageUrls')) { + playStackBlockHeavyUrls = normalizePlayStackSixUrlsFromTiming(payload.stackBlockHeavyImageUrls); + } + if (Object.prototype.hasOwnProperty.call(payload, 'stackHeavyBlockPercent')) { + const hp = Number(payload.stackHeavyBlockPercent); + if (Number.isFinite(hp)) { + playStackHeavyBlockPercent = Math.max(0, Math.min(100, Math.floor(hp))); + } else { + playStackHeavyBlockPercent = 35; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'jumpSurviveJumpHeightMult')) { + const jm = Number(payload.jumpSurviveJumpHeightMult); + if (Number.isFinite(jm)) { + playJumpSurviveJumpHeightMult = Math.round(Math.max(0.5, Math.min(4, jm)) * 100) / 100; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'jumpSurviveMissionTimeSec')) { + const jt = Number(payload.jumpSurviveMissionTimeSec); + if (Number.isFinite(jt) && jt > 0) { + playJumpSurviveMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(jt))); + } else { + playJumpSurviveMissionTimeSec = 0; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'jumpSurvivePlatformTiles') + || Object.prototype.hasOwnProperty.call(payload, 'jumpSurvivePlatformTileUrl')) { + const legacy = Object.prototype.hasOwnProperty.call(payload, 'jumpSurvivePlatformTileUrl') + ? normalizeGauntletAssetUrlForPlay(typeof payload.jumpSurvivePlatformTileUrl === 'string' ? payload.jumpSurvivePlatformTileUrl : '') + : ''; + const arr = Array.isArray(payload.jumpSurvivePlatformTiles) ? payload.jumpSurvivePlatformTiles : []; + const next = []; + for (let pi = 0; pi < 3; pi++) { + const o = arr[pi] && typeof arr[pi] === 'object' ? arr[pi] : {}; + let url = normalizeGauntletAssetUrlForPlay(typeof o.url === 'string' ? o.url : ''); + if (pi === 0 && !url && legacy) url = legacy; + let ww = Number(o.w); + let hh = Number(o.h); + if (!Number.isFinite(ww) || ww <= 0) ww = 0; + else ww = Math.max(8, Math.min(4096, Math.round(ww))); + if (!Number.isFinite(hh) || hh <= 0) hh = 0; + else hh = Math.max(8, Math.min(4096, Math.round(hh))); + next.push({ url, w: ww, h: hh }); + if (url) ensureGauntletAssetImage(url); + } + playJumpSurvivePlatformTiles = next; + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterMissionTimeSec')) { + const st = Number(payload.spaceShooterMissionTimeSec); + if (Number.isFinite(st) && st > 0) { + playSpaceShooterMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(st))); + } else { + playSpaceShooterMissionTimeSec = 0; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterShipImageUrls')) { + const a = payload.spaceShooterShipImageUrls; + const next = ['', '', '', '', '', '']; + if (Array.isArray(a)) { + for (let si = 0; si < 6; si++) { + const u = normalizeGauntletAssetUrlForPlay(typeof a[si] === 'string' ? a[si] : ''); + next[si] = u; + if (u) ensureGauntletAssetImage(u); + } + } + playSpaceShooterShipImageUrls = next; + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterAsteroidSpriteUrls')) { + const raw = payload.spaceShooterAsteroidSpriteUrls; + const next = []; + if (Array.isArray(raw)) { + for (let ai = 0; ai < raw.length && next.length < 32; ai++) { + const u = normalizeGauntletAssetUrlForPlay(typeof raw[ai] === 'string' ? raw[ai] : ''); + if (u) next.push(u); + } + } + playSpaceShooterAsteroidSpriteUrls = next; + next.forEach((u) => { + if (u) ensureGauntletAssetImage(u); + }); + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterAsteroidExplodeFrameMs')) { + const ef = Number(payload.spaceShooterAsteroidExplodeFrameMs); + if (Number.isFinite(ef)) { + playSpaceShooterAsteroidExplodeFrameMs = Math.max(30, Math.min(500, Math.round(ef))); + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterAsteroidIntervalMs')) { + const iv = Number(payload.spaceShooterAsteroidIntervalMs); + if (Number.isFinite(iv) && iv >= 200) { + playSpaceShooterAsteroidIntervalMs = Math.min(10000, Math.floor(iv)); + } else { + playSpaceShooterAsteroidIntervalMs = 1040; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'spaceShooterShipDamageOverlayUrls')) { + const rawD = payload.spaceShooterShipDamageOverlayUrls; + const nextD = ['', '', '']; + if (Array.isArray(rawD)) { + for (let di = 0; di < 3; di++) { + const u = normalizeGauntletAssetUrlForPlay(typeof rawD[di] === 'string' ? rawD[di] : ''); + nextD[di] = u; + if (u) ensureGauntletAssetImage(u); + } + } + playSpaceShooterShipDamageOverlayUrls = nextD; + } + if (Object.prototype.hasOwnProperty.call(payload, 'balloonBossMissionTimeSec')) { + const st = Number(payload.balloonBossMissionTimeSec); + if (Number.isFinite(st) && st > 0) { + playBalloonBossMissionTimeSec = Math.max(10, Math.min(7200, Math.floor(st))); + } else { + playBalloonBossMissionTimeSec = 0; + } + } + if (Object.prototype.hasOwnProperty.call(payload, 'balloonBossBossImageUrl')) { + playBalloonBossBossImageUrl = normalizeGauntletAssetUrlForPlay( + typeof payload.balloonBossBossImageUrl === 'string' ? payload.balloonBossBossImageUrl : '' + ); + if (playBalloonBossBossImageUrl) ensureGauntletAssetImage(playBalloonBossBossImageUrl); + } + if (Object.prototype.hasOwnProperty.call(payload, 'balloonBossPlayerBalloonImageUrls')) { + const a = payload.balloonBossPlayerBalloonImageUrls; + const nextBb = ['', '', '', '', '', '']; + if (Array.isArray(a)) { + for (let si = 0; si < 6; si++) { + const u = normalizeGauntletAssetUrlForPlay(typeof a[si] === 'string' ? a[si] : ''); + nextBb[si] = u; + if (u) ensureGauntletAssetImage(u); + } + } + playBalloonBossPlayerBalloonImageUrls = nextBb; + } + if (Object.prototype.hasOwnProperty.call(payload, 'balloonBossPlayerBalloonFallbackUrl')) { + playBalloonBossPlayerBalloonFallbackUrl = normalizeGauntletAssetUrlForPlay( + typeof payload.balloonBossPlayerBalloonFallbackUrl === 'string' ? payload.balloonBossPlayerBalloonFallbackUrl : '' + ); + if (playBalloonBossPlayerBalloonFallbackUrl) ensureGauntletAssetImage(playBalloonBossPlayerBalloonFallbackUrl); + } + if (isStack() && stackMini) reapplyStackMiniSizingFromGlobals(); + } + + /** เลอร์ปตำแหน่งตัวละครต่อเฟรม (ยิ่งสูงยิ่งตามเป้าเร็ว) */ + const GAUNTLET_VIS_LERP = 0.42; + + function gauntletObsSmoothstep(t) { + t = Math.max(0, Math.min(1, t)); + return t * t * (3 - 2 * t); + } + + function cloneGauntletObsSnap(arr) { + if (!Array.isArray(arr)) return []; + return arr.map((o) => { + if (!o || o.id == null) return null; + const row = { id: o.id, kind: o.kind, x: o.x, y: o.y }; + if (o.kind === 'laser') { + if (o.y0 != null) row.y0 = o.y0; + if (o.y1 != null) row.y1 = o.y1; + } + return row; + }).filter(Boolean); + } + + function gauntletLaserRowHitRange(ob, mapH) { + const h = Math.max(1, Math.floor(Number(mapH)) || 15); + let y0 = ob && ob.y0 != null && Number.isFinite(Number(ob.y0)) ? Math.floor(Number(ob.y0)) : 0; + let y1 = ob && ob.y1 != null && Number.isFinite(Number(ob.y1)) ? Math.floor(Number(ob.y1)) : h - 1; + y0 = Math.max(0, Math.min(h - 1, y0)); + y1 = Math.max(0, Math.min(h - 1, y1)); + if (y1 < y0) { + const t = y0; + y0 = y1; + y1 = t; + } + return { y0, y1 }; + } + + function gauntletLaserOverlapsPlayerRow(ob, py, mapH) { + const { y0, y1 } = gauntletLaserRowHitRange(ob, mapH); + return py >= y0 && py <= y1; + } + + /** โค้งยกตัว: ขึ้นถึงจุดสูงสุดเร็ว ลงชัน (รู้สึกลงพื้นเร็วกว่าแบบสมมาตร) */ + function gauntletLiftHeightNorm(air, jumpTicksMax) { + const jm = Math.max(4, jumpTicksMax || 16); + const a = Math.max(0, Math.min(jm, Number(air) || 0)); + const t = a / jm; + const peakAt = 0.28; + if (t >= peakAt) { + const span = 1 - peakAt; + const u = span > 0 ? (t - peakAt) / span : 0; + return Math.max(0, 1 - u * u * 1.2); + } + return Math.min(1, t / peakAt); + } + + function getGauntletObsDrawPositionsAt(nowMs) { + if (!gauntletObsRenderNext.length) return []; + const elapsed = nowMs - gauntletObsBlendT0; + const alpha = gauntletObsSmoothstep(elapsed / gauntletRuntimeTickMs); + const prevMap = new Map(gauntletObsRenderPrev.map((o) => [o.id, o])); + const out = []; + for (let i = 0; i < gauntletObsRenderNext.length; i++) { + const n = gauntletObsRenderNext[i]; + if (!n) continue; + const p = prevMap.get(n.id); + let drawX = n.x; + if (p && Number.isFinite(p.x) && Number.isFinite(n.x)) drawX = p.x + (n.x - p.x) * alpha; + const row = { id: n.id, kind: n.kind, drawX, y: n.y }; + if (n.kind === 'laser') { + if (n.y0 != null) row.y0 = n.y0; + if (n.y1 != null) row.y1 = n.y1; + } + out.push(row); + } + return out; + } + + function pushGauntletObsRenderFrame(newObs) { + const now = performance.now(); + const next = cloneGauntletObsSnap(newObs); + if (!gauntletObsRenderNext.length) { + gauntletObsRenderPrev = next.slice(); + gauntletObsRenderNext = next.slice(); + } else { + const curDraw = getGauntletObsDrawPositionsAt(now); + const curMap = new Map(curDraw.map((o) => [o.id, o.drawX])); + gauntletObsRenderPrev = next.map((n) => { + const row = { + id: n.id, + kind: n.kind, + x: curMap.has(n.id) ? curMap.get(n.id) : n.x, + y: n.y, + }; + if (n.kind === 'laser') { + if (n.y0 != null) row.y0 = n.y0; + if (n.y1 != null) row.y1 = n.y1; + } + return row; + }); + gauntletObsRenderNext = next; + } + gauntletObsBlendT0 = now; + } + + function lerpGauntletEntityPos(x, y, tx, ty) { + let nx = x; + let ny = y; + if (tx != null && Number.isFinite(tx)) { + nx += (tx - nx) * GAUNTLET_VIS_LERP; + if (Math.abs(tx - nx) < 0.02) nx = tx; + } + if (ty != null && Number.isFinite(ty)) { + ny += (ty - ny) * GAUNTLET_VIS_LERP; + if (Math.abs(ty - ny) < 0.02) ny = ty; + } + return { nx, ny }; + } + + /** + * บอท preview ใน Gauntlet: ตัดสินใจกระโดด (client-only; เซิร์ฟเวอร์ไม่รู้จักบอท) + * — ตอบสนองสิ่งกีดขวางที่ชนเซลล์เดียวกัน + กระโดดล่วงหน้าเมื่อ threat อยู่คอลัมน์ถัดไป + */ + function maybePreviewBotGauntletJump(o, obstacles, w, h) { + if (isGauntletCrownHeistMapPlay() && o.gauntletEliminated) return; + let px = Math.floor(Number(o.x)) || 0; + let py = Math.floor(Number(o.y)) || 0; + px = Math.max(0, Math.min(w - 1, px)); + py = Math.max(0, Math.min(h - 1, py)); + if ((o.gauntletJumpTicks || 0) > 0) return; + const { ch: gauntletCh } = getCharacterFootprintWH(mapData); + let sameCell = false; + let incomingCol = false; + for (let i = 0; i < obstacles.length; i++) { + const obs = obstacles[i]; + if (!obs) continue; + if (obs.kind === 'lane' && typeof obs.y === 'number') { + if (obs.x === px && obs.y >= py && obs.y < py + gauntletCh) sameCell = true; + if (obs.x === px + 1 && obs.y >= py && obs.y < py + gauntletCh) incomingCol = true; + } + if (obs.kind === 'laser' && typeof obs.x === 'number') { + if (obs.x === px && gauntletLaserOverlapsPlayerRow(obs, py, h)) sameCell = true; + if (obs.x === px + 1 && gauntletLaserOverlapsPlayerRow(obs, py, h)) incomingCol = true; + } + } + const tier = o.botTier || 'avg'; + const roll = Math.random(); + const pSame = tier === 'sharp' ? 0.96 : tier === 'weak' ? 0.62 : 0.86; + const pAhead = tier === 'sharp' ? 0.9 : tier === 'weak' ? 0.48 : 0.72; + if (sameCell && roll < pSame) { + o.gauntletJumpTicks = gauntletRuntimeJumpTicks; + return; + } + if (incomingCol && roll < pAhead) o.gauntletJumpTicks = gauntletRuntimeJumpTicks; + } + + /** หนึ่ง tick เดียวกับ runGauntletTick ฝั่งเซิร์ฟเวอร์ (เฉพาะ collision + เลื่อน x) */ + function applyGauntletPhysicsToPreviewBot(o, obstacles, w, h) { + if (isGauntletCrownHeistMapPlay() && o.gauntletEliminated) return; + let px = Math.floor(Number(o.x)) || 0; + let py = Math.floor(Number(o.y)) || 0; + px = Math.max(0, Math.min(w - 1, px)); + py = Math.max(0, Math.min(h - 1, py)); + const air = (o.gauntletJumpTicks || 0) > 0; + const { ch: gauntletCh2 } = getCharacterFootprintWH(mapData); + const crown = isGauntletCrownHeistMapPlay(); + let advanceX = false; + let hitBack = false; + for (let i = 0; i < obstacles.length; i++) { + const ob = obstacles[i]; + if (!ob) continue; + if (ob.kind === 'lane' && typeof ob.y === 'number' && ob.x === px && ob.y >= py && ob.y < py + gauntletCh2) { + if (air) advanceX = true; + else hitBack = true; + } + if (ob.kind === 'laser' && typeof ob.x === 'number' && ob.x === px) { + if (!gauntletLaserOverlapsPlayerRow(ob, py, h)) { + /* เลเซอร์ไม่ครอบแถวนี้ */ + } else if (air) { + advanceX = true; + } else { + hitBack = true; + } + } + } + if (advanceX) { + px = Math.min(w - 2, px + 1); + o.gauntletJumpTicks = 0; + if (!crown) { + o.gauntletScore = (o.gauntletScore || 0) + 1; + } + } else if (hitBack) { + if (crown) { + if (px <= 0) { + o.gauntletEliminated = true; + o.gauntletScore = 0; + } else { + o.gauntletScore = Math.max(0, (o.gauntletScore || 0) - 10); + o.gauntletCrownPenaltyFxUntil = Date.now() + 1100; + px = Math.max(0, px - 1); + } + } else { + px = Math.max(0, px - 1); + } + } + if (crown && mapData) { + const mx = getGauntletCrownHeistMaxEntityXPlay(mapData); + if (mx != null && Number.isFinite(mx)) { + const maxPx = Math.floor(mx + 1e-6); + px = Math.min(px, maxPx); + } + } + if ((o.gauntletJumpTicks || 0) > 0) o.gauntletJumpTicks--; + o.tx = px; + o.ty = py; + } + + function applyGauntletPreviewBotsAfterSync() { + if (!playBotsEnabled() || !mapData || mapData.gameType !== 'gauntlet') return; + if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return; + const w = mapData.width || 20; + const h = mapData.height || 15; + others.forEach((o, id) => { + if (!isPreviewBotId(id) || !o) return; + maybePreviewBotGauntletJump(o, gauntletObstacles, w, h); + applyGauntletPhysicsToPreviewBot(o, gauntletObstacles, w, h); + }); + } + + /** แจ้งเซิร์ฟเวอร์แถว y ที่มีคุณ+บอททดสอบ — lane spawn ใช้เฉพาะแถวที่มี peer; บอทไม่ใช่ peer */ + function emitGauntletPreviewRowsToServer() { + if (!playBotsEnabled() || !mapData || mapData.gameType !== 'gauntlet') return; + if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) return; + const h = mapData.height || 15; + const ysSet = new Set(); + const addY = (v) => { + const fy = Math.floor(Number(v)); + if (Number.isFinite(fy) && fy >= 0 && fy < h) ysSet.add(fy); + }; + addY(me.y); + others.forEach((o, id) => { + if (isPreviewBotId(id)) addY(o.y); + }); + socket.emit('gauntlet-preview-rows', { ys: [...ysSet] }); + } + + /** ซ่อน overlay โหมดอื่นที่ค้างจาก embed / session ก่อน — กัน UI ซ้อนไม่ตรง mock */ + function hideConflictingOverlaysForGauntletCrown() { + hideStackTowerResultFlashDomOnlyPlay(); + hideQuizCarryPregameOverlay(); + quizCarryPregameActive = false; + const hideIds = [ + 'quiz-game-overlay', + 'quiz-carry-mission-overlay', + 'gauntlet-ended-overlay', + 'quiz-battle-mcq-overlay', + 'quiz-carry-embed-countdown', + 'gauntlet-crown-countdown', + 'quiz-carry-embed-q-strip', + 'play-quiz-scoreboard', + 'play-quiz-feedback', + ]; + for (let i = 0; i < hideIds.length; i++) { + const el = document.getElementById(hideIds[i]); + if (el) el.classList.add('is-hidden'); + } + const gcm = document.getElementById('gauntlet-crown-mission-overlay'); + if (gcm) gcm.classList.add('is-hidden'); + hideStackTowerHowtoRulesDom(); + const gchHowto = document.getElementById('gauntlet-crown-howto-overlay'); + if (gchHowto) gchHowto.classList.remove(GCHOWTO_STACK_TOWER_CLASS); + } + + function gcmRankBadgeUrl(rank) { + const r = Math.floor(Number(rank)) || 99; + if (r === 1) return BASE + '/img/gauntlet-assets/result-1st.png'; + if (r === 2) return BASE + '/img/gauntlet-assets/result-2nd.png'; + if (r === 3) return BASE + '/img/gauntlet-assets/result-3rd.png'; + return BASE + '/img/gauntlet-assets/result-txt-2.png'; + } + + function gauntletCrownPregameReadyNumerator() { + let n = quizCarryPregameBotCount(); + quizCarryPregameHumanIds().forEach((id) => { + if (gauntletCrownLobbyReadyMap[id]) n++; + }); + return n; + } + + function gauntletCrownSyncGuestReadyIfNeeded() { + const inQuizQuestionHowto = isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto'; + const inStackTowerHowto = isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto'; + const inJumpSurviveHowto = isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto'; + const inSpaceShooterHowto = isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto'; + if (gauntletCrownPregamePhase !== 'howto' && !inQuizQuestionHowto && !inStackTowerHowto && !inJumpSurviveHowto && !inSpaceShooterHowto) return; + if (myId == null || isMePlayHost()) return; + const sid = String(myId); + if (gauntletCrownLobbyReadyMap[sid]) return; + gauntletCrownLobbyReadyMap[sid] = true; + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: true }); + } + + /** ภารกิจคำถาม mng8a80o — Ready Status / ปุ่ม แบบเดียวกับ Mega Virus (gauntlet-crown-lobby-sync) */ + function updateQuizQuestionMissionHowtoHud() { + if (!isQuizQuestionMissionUiMapPlay() || quizQuestionMissionPhase !== 'howto') return; + const st = document.getElementById('gauntlet-crown-howto-status'); + const btn = document.getElementById('btn-gch-ready'); + if (!st || !btn) return; + const humans = quizCarryPregameHumanIds(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + const num = gauntletCrownPregameReadyNumerator(); + st.classList.remove('is-hidden'); + st.textContent = 'Ready Status : ' + num + '/' + tot; + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + btn.classList.toggle('is-start-phase', humansReady); + btn.classList.toggle('is-read-only', !isMePlayHost()); + btn.disabled = !isMePlayHost(); + btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false')); + btn.title = isMePlayHost() + ? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + + function updateGauntletCrownHowtoHud() { + const st = document.getElementById('gauntlet-crown-howto-status'); + const btn = document.getElementById('btn-gch-ready'); + if (!btn || !usesCrownLobbyShellPlay() || gauntletCrownPregamePhase !== 'howto') return; + const humans = quizCarryPregameHumanIds(); + const tot = Math.max(1, quizCarryPregameTotalPlayers()); + const num = gauntletCrownPregameReadyNumerator(); + if (st) { + st.classList.remove('is-hidden'); + st.textContent = 'Ready Status : ' + num + '/' + tot; + } + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + btn.classList.toggle('is-start-phase', humansReady); + btn.classList.toggle('is-read-only', !isMePlayHost()); + btn.disabled = !isMePlayHost(); + btn.setAttribute('aria-pressed', humansReady ? 'false' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'true' : 'false')); + btn.title = isMePlayHost() + ? (humansReady ? 'START' : ((myId && gauntletCrownLobbyReadyMap[String(myId)]) ? 'ยกเลิก READY' : 'READY')) + : (humansReady ? 'START (โฮสต์เท่านั้น)' : 'READY (โฮสต์เท่านั้น)'); + } + + function beginGauntletCrownCountdownThenRun() { + if (!usesCrownLobbyShellPlay()) return; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + gauntletCrownPregamePhase = 'countdown'; + if (playBotsEnabled() && mapData && mapData.gameType === 'gauntlet') { + applyGauntletPreviewSpawnLayout(false); + } + trySnapGauntletCrownRunwayScrollToSpawnsPlay(); + const howto = document.getElementById('gauntlet-crown-howto-overlay'); + if (howto) howto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + const cd = document.getElementById('gauntlet-crown-countdown'); + const numEl = document.getElementById('gauntlet-crown-countdown-num'); + function applyMegaBalloonLiveStartPlay() { + balloonBossSessionStartMs = performance.now(); + balloonBossLastTickMs = performance.now(); + balloonBossGameEnded = false; + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + balloonBossBossFireAcc = 0; + balloonBossScorePopups = []; + me.balloonBossScore = 0; + me.balloonBossBossDmg = 0; + me.balloonBossBalloons = balloonBossBalloonsStartPlay(); + me.balloonBossEliminated = false; + others.forEach((o) => { + o.balloonBossScore = 0; + o.balloonBossBossDmg = 0; + o.balloonBossBalloons = balloonBossBalloonsStartPlay(); + o.balloonBossEliminated = false; + }); + if (mapData) applyBalloonBossSpawnLayoutPlay(); + } + const runFinish = () => { + if (cd) cd.classList.add('is-hidden'); + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + if (socket && socket.connected) { + socket.emit('gauntlet-crown-begin-run', (res) => { + if (res && res.ok) { + gauntletCrownPregamePhase = 'live'; + trySnapGauntletCrownRunwayScrollToSpawnsPlay(); + if (isMegaVirusMissionShellMapPlay()) applyMegaBalloonLiveStartPlay(); + } + }); + } else { + gauntletCrownPregamePhase = 'live'; + trySnapGauntletCrownRunwayScrollToSpawnsPlay(); + if (isMegaVirusMissionShellMapPlay()) applyMegaBalloonLiveStartPlay(); + } + }; + if (!cd || !numEl) { + runFinish(); + return; + } + cd.classList.remove('is-hidden'); + let n = 3; + setCountdown321QuestionAssetGraphic(numEl, n); + const step = () => { + n--; + if (n > 0) { + setCountdown321QuestionAssetGraphic(numEl, n); + gauntletCrownCountdownTimer = setTimeout(step, 1000); + } else { + gauntletCrownCountdownTimer = null; + runFinish(); + } + }; + gauntletCrownCountdownTimer = setTimeout(step, 1000); + } + + function showGauntletCrownHowtoOverlay() { + if (!usesCrownLobbyShellPlay()) return; + hideConflictingOverlaysForGauntletCrown(); + if (isMegaVirusMissionShellMapPlay()) applyMegaVirusMissionPanelImages(); + const cd = document.getElementById('gauntlet-crown-countdown'); + if (cd) cd.classList.add('is-hidden'); + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + gauntletCrownPregamePhase = 'howto'; + resetGauntletCrownRunwaySpawnScrollSnap(); + const ov = document.getElementById('gauntlet-crown-howto-overlay'); + if (!ov) return; + gauntletCrownHowtoVisible = true; + ov.classList.remove('is-hidden'); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-sync-request'); + gauntletCrownSyncGuestReadyIfNeeded(); + updateGauntletCrownHowtoHud(); + } + + /** Editor embed: quiz_carry / crown ใช้เกรด F = gameover — Jumper: ภาพจบใช้เฉพาะเมื่อไม่มีผู้รอด (0 คน) */ + function gauntletCrownEmbedMissionAnyOk(mission) { + if (!mission) return false; + if (mission.uiSkin === 'violent_crime') { + const sc = Number(mission.survivorCount); + if (Number.isFinite(sc) && sc <= 0) return false; + const g = String(mission.grade || '').trim().toUpperCase().charAt(0); + return g !== 'F'; + } + if (mission.uiSkin === 'jumper') { + const sc = Number(mission.survivorCount); + return Number.isFinite(sc) ? sc > 0 : (String(mission.grade || '').trim().toUpperCase().charAt(0) !== 'F'); + } + if (mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower') { + const g = String(mission.grade || '').trim().toUpperCase().charAt(0); + return g !== 'F'; + } + if (mission.uiSkin === 'mega_virus') { + const g = String(mission.grade || '').trim().toUpperCase().charAt(0); + return g !== 'F'; + } + const g = String(mission.grade || '').trim().toUpperCase().charAt(0); + return g !== 'F'; + } + + function gauntletCrownRankBonusLocal(rankOrdinal) { + if (rankOrdinal === 1) return 100; + if (rankOrdinal === 2) return 80; + if (rankOrdinal === 3) return 60; + return 0; + } + + function gauntletCrownRollRewardCardLocal(grade) { + const r = Math.random(); + if (grade === 'A') { + if (r < 0.3) return { kind: 'culprit', th: 'การ์ดชี้คนร้าย', en: 'Culprit card' }; + if (r < 0.8) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (grade === 'B') { + if (r < 0.2) return { kind: 'culprit', th: 'การ์ดชี้คนร้าย', en: 'Culprit card' }; + if (r < 0.5) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + if (r < 0.2) return { kind: 'rare', th: 'การ์ด Rare', en: 'Rare card' }; + return { kind: 'common', th: 'การ์ดธรรมดา', en: 'Common card' }; + } + + /** พรีวิว: สรุปภารกิจ Last Light หลังรันเวย์ครบ — ให้ gauntletCrownBuildDisplayMission รวมบอท */ + function gauntletCrownHeistBuildLocalCrownMissionPlay() { + const baseRows = []; + if (myId != null) { + baseRows.push({ + id: myId, + nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', + characterId: me.characterId ?? null, + baseScore: me.gauntletEliminated ? 0 : Math.max(0, Number(me.gauntletScore) | 0), + eliminated: !!me.gauntletEliminated, + }); + } + others.forEach((o, id) => { + if (!o || isPreviewBotId(id)) return; + baseRows.push({ + id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : String(id), + characterId: o.characterId ?? null, + baseScore: o.gauntletEliminated ? 0 : Math.max(0, Number(o.gauntletScore) | 0), + eliminated: !!o.gauntletEliminated, + }); + }); + baseRows.sort((a, b) => { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + const nick = String(a.nickname).localeCompare(String(b.nickname), 'th'); + if (nick !== 0) return nick; + return String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map((row, idx) => { + const pos = idx + 1; + const rankBonus = gauntletCrownRankBonusLocal(pos); + const bs = Math.max(0, Number(row.baseScore) | 0); + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: bs, + eliminated: !!row.eliminated, + rank: pos, + rankLabel: pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos), + rankBonus, + finalScore: bs + rankBonus, + }; + }); + const totalSum = ranked.reduce((s, r) => s + r.finalScore, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + const survivorCount = baseRows.filter((r) => !r.eliminated).length; + let grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + if (survivorCount <= 0) grade = 'F'; + const rewardCard = grade === 'F' ? null : gauntletCrownRollRewardCardLocal(grade); + return { + ranked, + totalSum, + averageScore, + grade, + rewardCard, + totalParts: ranked.map((r) => r.finalScore), + participantCount: baseRows.length, + survivorCount, + }; + } + + /** + * Editor embed + preview bots: รวมแถวอันดับกับบอท (สูงสุด 5 ช่อง) และคำนวณรวม/เฉลี่ย/เกรดให้สอดคล้องกับแถวที่โชว์ + * — อันดับในช่องใช้ลำดับ 1..5 หลังเรียงคะแนน (เหมือนแนว quiz_carry mission row) + */ + function gauntletCrownBuildDisplayMission(mission) { + if (!mission || !Array.isArray(mission.ranked)) return mission; + if (mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime' || mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus') return mission; + if (!(playBotsEnabled() && isGauntletCrownHeistMapPlay() && others && typeof others.forEach === 'function')) return mission; + + const seen = new Set(); + const baseRows = []; + mission.ranked.forEach((r) => { + if (!r) return; + const sid = String(r.id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: r.id, + nickname: (r.nickname && String(r.nickname).trim()) ? String(r.nickname).trim() : 'ผู้เล่น', + characterId: r.characterId ?? null, + baseScore: r.eliminated ? 0 : Math.max(0, Number(r.baseScore) || 0), + eliminated: !!r.eliminated, + }); + }); + others.forEach((o, id) => { + if (!o || !isPreviewBotId(id)) return; + const sid = String(id); + if (seen.has(sid)) return; + seen.add(sid); + baseRows.push({ + id: id, + nickname: (o.nickname && String(o.nickname).trim()) ? String(o.nickname).trim() : sid, + characterId: o.characterId ?? null, + baseScore: o.gauntletEliminated ? 0 : Math.max(0, Number(o.gauntletScore) | 0), + eliminated: !!o.gauntletEliminated, + }); + }); + + baseRows.sort((a, b) => { + if (b.baseScore !== a.baseScore) return b.baseScore - a.baseScore; + const nick = String(a.nickname).localeCompare(String(b.nickname), 'th'); + if (nick !== 0) return nick; + return String(a.id).localeCompare(String(b.id)); + }); + const top = baseRows.slice(0, 5); + const ranked = top.map((row, idx) => { + const pos = idx + 1; + const rankBonus = gauntletCrownRankBonusLocal(pos); + const rankLabel = pos === 1 ? '1st' : pos === 2 ? '2nd' : pos === 3 ? '3rd' : String(pos); + const finalScore = row.baseScore + rankBonus; + return { + id: row.id, + nickname: row.nickname, + characterId: row.characterId, + baseScore: row.baseScore, + eliminated: row.eliminated, + rank: pos, + rankLabel: rankLabel, + rankBonus: rankBonus, + finalScore: finalScore, + }; + }); + + const totalSum = ranked.reduce(function (s, r) { return s + r.finalScore; }, 0); + const n = ranked.length || 1; + const averageScore = Math.floor(totalSum / n); + const grade = averageScore >= 80 ? 'A' : averageScore >= 60 ? 'B' : 'C'; + const rewardCard = gauntletCrownRollRewardCardLocal(grade); + return { + ranked: ranked, + totalSum: totalSum, + averageScore: averageScore, + grade: grade, + rewardCard: rewardCard, + totalParts: ranked.map(function (r) { return r.finalScore; }), + }; + } + + function showGauntletCrownMissionOverlay(mission) { + const ov = document.getElementById('gauntlet-crown-mission-overlay'); + const rowEl = document.getElementById('gcm-rank-row'); + const totalEl = document.getElementById('gcm-total'); + const gradeEl = document.getElementById('gcm-grade'); + const bonusEl = document.getElementById('gcm-bonus'); + const btn = document.getElementById('btn-gcm-done'); + if (!ov || !rowEl || !mission || !Array.isArray(mission.ranked)) return; + let disp = gauntletCrownBuildDisplayMission(mission); + if (mission.uiSkin === 'jumper') { + disp = jumpSurviveMergePreviewBotsMission(disp) || disp; + } else if (mission.uiSkin === 'violent_crime') { + disp = spaceShooterMergePreviewBotsMission(disp) || disp; + } else if (mission.uiSkin === 'question_mission') { + disp = quizQuestionMissionMergePreviewBotsMission(disp) || disp; + } else if (mission.uiSkin === 'stack_tower') { + disp = stackTowerMissionMergePreviewBotsMission(disp) || disp; + } else if (mission.uiSkin === 'mega_virus') { + disp = balloonBossMergePreviewBotsMission(disp) || disp; + } + if (!disp || !Array.isArray(disp.ranked)) return; + rowEl.innerHTML = ''; + disp.ranked.forEach((r) => { + const cell = document.createElement('div'); + cell.className = 'gcm-cell'; + const tag = document.createElement('div'); + tag.className = 'gcm-rank-tag'; + const rk = Math.floor(Number(r.rank)) || 0; + if (rk >= 1 && rk <= 3) { + const tagImg = document.createElement('img'); + tagImg.src = gcmRankBadgeUrl(rk); + tagImg.alt = '[' + String(r.rankLabel || rk) + ']'; + tagImg.style.maxWidth = '100%'; + tagImg.style.height = 'auto'; + tag.appendChild(tagImg); + } else { + tag.textContent = '[' + String(r.rankLabel || rk) + ']'; + } + const frame = document.createElement('div'); + frame.className = 'gcm-frame'; + const img = document.createElement('img'); + img.className = 'gcm-av'; + img.alt = ''; + const cid = r.characterId ? String(r.characterId) : ''; + if (cid) { + setCyberHudScoreAvatarImg(img, { + id: r.id, + characterId: cid, + isMe: myId != null && r.id != null && String(r.id) === String(myId), + }); + } else { + img.src = defaultAvatarImg.src; + } + img.onerror = function () { + this.onerror = null; + this.src = defaultAvatarImg.src; + }; + frame.appendChild(img); + const qMissionLose = mission && mission.uiSkin === 'question_mission' && r.hadQuizWrong; + if (r.eliminated || qMissionLose) { + const st = document.createElement('img'); + st.className = 'gcm-stamp'; + const jSkin = mission && mission.uiSkin === 'jumper'; + let loseUrl = BASE + '/img/gauntlet-assets/result-Lose_stamp.png'; + let altLose = 'เสียชีวิต'; + if (jSkin && r.eliminated) { + loseUrl = jumperAssetUrl('stamp-sacrifice.png'); + altLose = 'ผู้เสียสละ'; + } else if (qMissionLose) { + loseUrl = BASE + '/img/QUESTION/result-Lose_stamp.png'; + altLose = 'ตอบผิด'; + } + st.src = loseUrl; + st.alt = altLose; + st.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/result-Lose_stamp.png'; + this.alt = 'ตอบผิด'; + }; + frame.appendChild(st); + } + const nick = document.createElement('div'); + nick.className = 'gcm-nick'; + nick.textContent = r.nickname || '—'; + const sc = document.createElement('div'); + sc.className = 'gcm-sc'; + const jumpPlain = mission && (mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime' || mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus'); + const botScorePlain = (playBotsEnabled() && isGauntletCrownHeistMapPlay() && isPreviewBotId(r.id)) + || (playBotsEnabled() && mission && mission.uiSkin === 'question_mission' && isPreviewBotId(r.id)) + || (playBotsEnabled() && mission && mission.uiSkin === 'stack_tower' && isPreviewBotId(r.id)) + || (playBotsEnabled() && mission && mission.uiSkin === 'mega_virus' && isPreviewBotId(r.id)); + sc.textContent = (jumpPlain || botScorePlain) + ? String(Math.max(0, Number(r.baseScore) || 0)) + : String(r.baseScore || 0) + '(+' + String(r.rankBonus || 0) + ')'; + cell.appendChild(tag); + cell.appendChild(frame); + cell.appendChild(nick); + cell.appendChild(sc); + rowEl.appendChild(cell); + }); + if (totalEl) { + const parts = (disp.totalParts || []).map((n) => String(Math.max(0, Number(n) || 0))); + const sum = Math.max(0, Number(disp.totalSum) || 0); + const avg = Math.max(0, Math.floor(Number(disp.averageScore) || 0)); + const jumperSkin = mission && mission.uiSkin === 'jumper'; + const violentSkin = mission && mission.uiSkin === 'violent_crime'; + const questionMissionSkin = mission && mission.uiSkin === 'question_mission'; + if (jumperSkin) { + const sc = Math.max(0, Math.floor(Number(disp.survivorCount) || 0)); + const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0)); + totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · ผู้รอด ' + sc + '/' + pc + ' → เกรด'; + } else if (violentSkin) { + const sc = Math.max(0, Math.floor(Number(disp.survivorCount) || 0)); + const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0)); + totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · รอดชีวิต ' + sc + '/' + pc + ' → เกรด'; + } else if (questionMissionSkin) { + const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0)); + totalEl.innerHTML = 'คะแนนตอบคำถาม (' + parts.join('+') + ') = ' + sum + ' · ผู้เล่น ' + pc + ' → เกรด'; + } else if (mission && mission.uiSkin === 'stack_tower') { + const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0)); + totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · ผู้เล่น ' + pc + ' → เกรด'; + } else if (mission && mission.uiSkin === 'mega_virus') { + const pc = Math.max(0, Math.floor(Number(disp.participantCount) || 0)); + totalEl.innerHTML = 'ความเสียหายรวม (' + parts.join('+') + ') = ' + sum + ' · ผู้เล่น ' + pc + ' → เกรด'; + } else { + totalEl.innerHTML = 'คะแนนรวม (' + parts.join('+') + ') = ' + sum + ' · เฉลี่ย ' + avg + ' → เกรด'; + } + } + if (gradeEl) { + const g = String(disp.grade || 'C').toUpperCase().charAt(0); + gradeEl.textContent = g; + gradeEl.className = 'gcm-grade gcm-grade--' + g.toLowerCase(); + } + if (bonusEl) { + bonusEl.innerHTML = ''; + const gLetter = String(disp.grade || 'C').toUpperCase().charAt(0); + const missionOk = gLetter !== 'F'; + if (missionOk) { + const row = document.createElement('div'); + row.style.display = 'flex'; + row.style.alignItems = 'center'; + row.style.gap = '8px'; + const chk = document.createElement('img'); + const stackTS = mission && mission.uiSkin === 'stack_tower'; + const megaVs = mission && mission.uiSkin === 'mega_virus'; + chk.src = stackTS ? stackTowerAssetUrl('result-check.png') : (megaVs ? megaVirusAssetUrl('result-check.png') : (BASE + '/img/gauntlet-assets/result-check.png')); + chk.alt = ''; + if (stackTS || megaVs) { + chk.onerror = function () { + this.onerror = null; + this.src = BASE + '/img/gauntlet-assets/result-check.png'; + }; + } + chk.width = 26; + chk.height = 26; + const sp = document.createElement('span'); + sp.style.color = '#9ece6a'; + sp.style.fontWeight = '700'; + sp.textContent = 'ภารกิจสำเร็จ'; + row.appendChild(chk); + row.appendChild(sp); + bonusEl.appendChild(row); + } + const rc = disp.rewardCard; + if (missionOk && rc && rc.th) { + const pl = document.createElement('div'); + pl.className = 'gcm-plaque'; + pl.innerHTML = '' + String(rc.th) + '' + String(rc.en || '') + ''; + bonusEl.appendChild(pl); + } else if (!missionOk) { + const pl = document.createElement('div'); + pl.className = 'gcm-plaque'; + pl.style.borderColor = 'rgba(148, 153, 168, 0.45)'; + pl.style.opacity = '0.95'; + pl.innerHTML = 'ไม่ได้รับการ์ดเกรด ' + gLetter + ' · ภารกิจไม่ผ่านเกณฑ์'; + bonusEl.appendChild(pl); + } + } + const gcmHead = document.getElementById('gcm-heading'); + if (gcmHead) { + if (mission && (mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus' || mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime')) gcmHead.classList.add('sr-only'); + else gcmHead.classList.remove('sr-only'); + } + ov.classList.remove('is-hidden'); + function goLobby() { + ov.classList.add('is-hidden'); + if (gcmHead) gcmHead.classList.remove('sr-only'); + if (tryFinishDetectiveMinigameAndReturnLobby()) return; + window.location.href = buildRoomLobbyReturnHref(); + } + if (btn) { + btn.onclick = function () { + if (previewMode && editorEmbedReturn) { + ov.classList.add('is-hidden'); + if (gcmHead) gcmHead.classList.remove('sr-only'); + if (mission && (mission.uiSkin === 'question_mission' || mission.uiSkin === 'stack_tower' || mission.uiSkin === 'mega_virus' || mission.uiSkin === 'jumper' || mission.uiSkin === 'violent_crime')) { + cancelQuizCarryResultEndAfterTimeup(); + hideQuizCarryTimeupOnDeskLayer(); + hideQuizCarryResultEndLayer(); + scheduleEmbedPreviewReturnToLobbyAfterResultEnd(); + } else { + showQuizCarryTimeupOnDeskLayer(function () { return gauntletCrownEmbedMissionAnyOk(disp); }); + } + } else { + goLobby(); + } + }; + } + } + + function isLobby() { return mapData && mapData.gameType === 'lobby'; } + function isQuiz() { return mapData && mapData.gameType === 'quiz'; } + function getLane(y) { + if (!mapData || !mapData.lanes) return null; + const lane = mapData.lanes.find(l => l.y === y); + return lane || (mapData.lanes[y] != null ? mapData.lanes[y] : null); + } + function getVehiclePositions(lane, width, timeMs) { + if (!lane || (lane.type !== 'road' && lane.type !== 'water')) return []; + const speed = (lane.speed != null ? lane.speed : 1) * 0.05; + const dir = lane.dir === -1 ? -1 : 1; + const spacing = lane.spacing != null ? lane.spacing : (lane.type === 'road' ? 3 : 2.5); + const period = width + 2; + const count = Math.max(2, Math.floor(period / spacing)); + const positions = []; + for (let i = 0; i < count; i++) { + const p = i * spacing + (timeMs * speed * dir) / 60; + const pNorm = ((p % period) + period) % period; + const vx = pNorm - 1; + positions.push(Math.max(-1, Math.min(width, vx))); + } + return positions; + } + function checkFroggerCollision() { + const fy = Math.floor(me.y); + const lane = getLane(fy); + if (!lane) return false; + if (lane.type === 'road') { + const positions = getVehiclePositions(lane, mapData.width, Date.now()); + for (let i = 0; i < positions.length; i++) { + if (Math.abs(positions[i] - me.x) < 1.1) return true; + } + } + if (lane.type === 'water') { + const positions = getVehiclePositions(lane, mapData.width, Date.now()); + let onLog = false; + for (let i = 0; i < positions.length; i++) { + if (Math.abs(positions[i] - me.x) < 1.2) { onLog = true; break; } + } + if (!onLog) return true; + } + return false; + } + function respawnFrogger() { + const sp = mapData.spawn || { x: 1, y: mapData.height - 1 }; + me.x = sp.x; me.y = sp.y; + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + } + + /** รองรับ x/y เป็น string จาก Socket — ถ้าเช็คแค่ typeof number จะร่วงไป mapData.spawn ทุกครั้ง (จุดเกิดไม่สุ่ม) */ + function peerXYFromJoin(peer, spawnFb) { + const sx = Number(spawnFb && spawnFb.x); + const sy = Number(spawnFb && spawnFb.y); + const defX = Number.isFinite(sx) ? sx : 1; + const defY = Number.isFinite(sy) ? sy : 1; + if (!peer) return { x: defX, y: defY }; + const x = Number(peer.x); + const y = Number(peer.y); + if (Number.isFinite(x) && Number.isFinite(y)) return { x, y }; + return { x: defX, y: defY }; + } + + /** Hub = 0/1 · กริดตัวเลือก = 0 หรือเลข 1..16 (ห้ามบังคับ === 1 เหมือน hub — เลข 2–16จะหาย) */ + function normalizeQuizCarryLayersInPlay(md) { + if (!md || md.gameType !== 'quiz_carry') return; + const w = md.width || 20, h = md.height || 15; + const maxOpt = QUIZ_CARRY_MAX_OPTION_SLOTS; + const srcHub = md.quizCarryHubArea || []; + const hubRows = []; + for (let y = 0; y < h; y++) { + const r = srcHub[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + hubRows.push(row); + } + md.quizCarryHubArea = hubRows; + const srcOpt = md.quizCarryOptionArea || []; + const optRows = []; + for (let y = 0; y < h; y++) { + const r = srcOpt[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + row.push(Number.isFinite(n) && n >= 1 && n <= maxOpt ? Math.floor(n) : 0); + } + optRows.push(row); + } + md.quizCarryOptionArea = optRows; + const srcCd = md.carryEmbedCountdownArea || []; + const cdRows = []; + for (let y = 0; y < h; y++) { + const r = srcCd[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + row.push(Number(v) === 1 ? 1 : 0); + } + cdRows.push(row); + } + md.carryEmbedCountdownArea = cdRows; + } + + /** โดม = ช่องติดกัน (4 ทิศ) ต่อ 1 ข้อ · comp id เรียงตามการค้นพบบนแมป */ + function normalizeQuizBattleDomeInPlay(md) { + if (!md || md.gameType !== 'quiz_battle') return; + const w = md.width || 20, h = md.height || 15; + const src = md.quizBattleDomeArea || []; + const grid = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + grid.push(row); + } + md.quizBattleDomeArea = grid; + const comp = Array(h).fill(0).map(() => Array(w).fill(0)); + let nextId = 0; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (grid[y][x] !== 1 || comp[y][x]) continue; + nextId++; + const stack = [[x, y]]; + comp[y][x] = nextId; + while (stack.length) { + const c = stack.pop(); + const cx = c[0], cy = c[1]; + for (let i = 0; i < 4; i++) { + const dx = (i === 0 ? 1 : i === 1 ? -1 : 0); + const dy = (i === 2 ? 1 : i === 3 ? -1 : 0); + const nx = cx + dx, ny = cy + dy; + if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue; + if (grid[ny][nx] !== 1 || comp[ny][nx]) continue; + comp[ny][nx] = nextId; + stack.push([nx, ny]); + } + } + } + } + md.quizBattleDomeComp = comp; + } + + /** เส้นทาง Quiz Battle — วาดอย่างน้อย 1 ช่องแล้วจะเดินได้เฉพาะบนเส้นทาง (เปลี่ยนแค่รูปพื้นหลังได้) */ + function normalizeQuizBattlePathInPlay(md) { + if (!md || md.gameType !== 'quiz_battle') return; + const w = md.width || 20, h = md.height || 15; + const src = md.quizBattlePathArea || []; + const grid = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + grid.push(row); + } + md.quizBattlePathArea = grid; + } + + function quizBattlePathModeActive(md) { + if (!md || md.gameType !== 'quiz_battle' || !md.quizBattlePathArea) return false; + const g = md.quizBattlePathArea; + for (let y = 0; y < g.length; y++) { + const row = g[y]; + if (!row) continue; + for (let x = 0; x < row.length; x++) if (row[x] === 1) return true; + } + return false; + } + + function quizBattleFootprintFullyOnPath(md, px, py) { + if (!md || !quizBattlePathModeActive(md)) return true; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return false; + const tiles = quizTilesFootprintPlay(px, py); + if (tiles.size === 0) return false; + const g = md.quizBattlePathArea; + for (const k of tiles) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (!g[ty] || g[ty][tx] !== 1) return false; + } + return true; + } + + /** สแน็ปบนเลน — เช็คแค่กำแพง + อยู่บน path (ให้ตรงกับ server) ไม่ใช้ blockPlayer/ผู้เล่นคนอื่น เพราะจะทำให้หาจุดสแน็ปไม่ได้แล้วค้างนอกเลน */ + function quizBattleSnapTargetValidPlay(x, y) { + if (!mapData || !mapData.objects) return false; + if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false; + const w = mapData.width || 20, h = mapData.height || 15; + for (const k of quizTilesWallCollisionFootprintPlay(x, y)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = mapData.objects[ty]; + if (!row || row[tx] === 1) return false; + } + if (quizBattlePathModeActive(mapData) && !quizBattleFootprintFullyOnPath(mapData, x, y)) return false; + return true; + } + + function snapPositionOntoQuizBattlePathIfNeeded(px, py) { + if (!mapData || !isQuizBattle() || !quizBattlePathModeActive(mapData)) return { x: px, y: py }; + if (quizBattleSnapTargetValidPlay(px, py)) return { x: px, y: py }; + const w = mapData.width || 20, h = mapData.height || 15; + const g = mapData.quizBattlePathArea; + let bestX = null, bestY = null, bestD = Infinity; + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + if (!g[ty] || g[ty][tx] !== 1) continue; + const nx = tx + 0.01; + const ny = ty + 0.01; + if (!quizBattleSnapTargetValidPlay(nx, ny)) continue; + const d = Math.abs(nx - px) + Math.abs(ny - py); + if (d < bestD) { bestD = d; bestX = nx; bestY = ny; } + } + } + if (bestX != null) return { x: bestX, y: bestY }; + /* footprint ใหญ่กว่าเลน (เช่น 2×2 บนทางแคบ 1 ช่อง): หาช่อง path ใกล้สุดที่เดินได้อย่างน้อยที่เซ็นเตอร์ — ดีกว่าปล่อยค้างนอกเลน */ + for (let ty = 0; ty < h; ty++) { + for (let tx = 0; tx < w; tx++) { + if (!g[ty] || g[ty][tx] !== 1) continue; + if (!spawnTileWalkablePlay(mapData, tx, ty)) continue; + const nx = tx + 0.5; + const ny = ty + 0.5; + const d = Math.abs(nx - px) + Math.abs(ny - py); + if (d < bestD) { bestD = d; bestX = nx; bestY = ny; } + } + } + if (bestX != null) return { x: bestX, y: bestY }; + return { x: px, y: py }; + } + + /** บังคับให้ผู้เล่นอยู่บนเลน — เรียกก่อน return กลาง tick (แชท / path หมด) เพราะเดิมข้ามบล็อกสแน็ปท้าย tick */ + function enforceQuizBattleLaneOnMePlay() { + if (!mapData || !isQuizBattle() || !quizBattlePathModeActive(mapData)) return; + if (quizBattleSnapTargetValidPlay(me.x, me.y)) return; + const snapped = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = snapped.x; + me.y = snapped.y; + } + + function hideQuizBattleMcqModal() { + const ov = document.getElementById('quiz-battle-mcq-overlay'); + if (ov) { + ov.classList.add('is-hidden'); + ov.setAttribute('aria-hidden', 'true'); + } + } + + function flashQuizBattleFeedback(text, ok) { + const el = document.getElementById('play-quiz-feedback'); + if (!el) return; + el.textContent = text || ''; + el.classList.remove('is-hidden', 'play-quiz-feedback-ok', 'play-quiz-feedback-bad'); + el.classList.add(ok ? 'play-quiz-feedback-ok' : 'play-quiz-feedback-bad'); + if (typeof window.__quizBattleFbT === 'number') clearTimeout(window.__quizBattleFbT); + window.__quizBattleFbT = setTimeout(() => { el.classList.add('is-hidden'); }, 1800); + } + + function getQuizBattleDomeCompIdsUnderFootprint(px, py) { + if (!mapData || !isQuizBattle()) return []; + const comp = mapData.quizBattleDomeComp; + const grid = mapData.quizBattleDomeArea; + if (!comp || !grid) return []; + const ids = []; + const seen = Object.create(null); + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + const cid = comp[ty] && comp[ty][tx]; + if (grid[ty] && grid[ty][tx] === 1 && cid > 0 && !seen[cid]) { + seen[cid] = 1; + ids.push(cid); + } + } + return ids; + } + + function getPrimaryQuizBattleDomeCompAt(px, py) { + const arr = getQuizBattleDomeCompIdsUnderFootprint(px, py); + if (!arr.length) return null; + return Math.min.apply(null, arr); + } + + function showQuizBattleMcqModal(q) { + const ov = document.getElementById('quiz-battle-mcq-overlay'); + const textEl = document.getElementById('quiz-battle-mcq-text'); + if (!ov || !textEl || !q) return; + textEl.textContent = q.text || ''; + const labels = ['A', 'B', 'C']; + for (let i = 0; i < 3; i++) { + const btn = ov.querySelector('.quiz-battle-choice[data-idx="' + i + '"]'); + if (btn) { + const ch = (q.choices && q.choices[i]) ? String(q.choices[i]) : ''; + btn.textContent = labels[i] + (ch ? (': ' + ch.slice(0, 96)) : ''); + } + } + ov.classList.remove('is-hidden'); + ov.setAttribute('aria-hidden', 'false'); + } + + function trySubmitQuizBattleChoice(idx) { + if (quizBattleModalCompId == null || !mapData || !isQuizBattle()) return; + const compId = quizBattleModalCompId; + const pool = quizBattleMcqPool; + if (!pool.length) { + flashQuizBattleFeedback('ยังไม่มีคำถาม — ตั้ง battleQuizMcq ใน Admin → Quiz Battle', false); + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + return; + } + const q = pool[(compId - 1) % pool.length]; + if (!q) return; + const ok = Number(q.correctIndex) === Number(idx); + if (ok) { + quizBattleAnsweredComps.add(compId); + if (myId != null) { + if (!playLiveQuizScores) playLiveQuizScores = {}; + playLiveQuizScores[myId] = (playLiveQuizScores[myId] || 0) + 1; + renderPlayQuizScoreboard(playLiveQuizScores); + } + flashQuizBattleFeedback('ถูกต้อง · +1', true); + } else { + flashQuizBattleFeedback('ยังไม่ถูก — ลองใหม่ (กด E อีกครั้ง)', false); + } + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + } + + function tryOpenQuizBattleFromKey() { + if (!isQuizBattle() || !mapData || myId == null) return; + const compId = getPrimaryQuizBattleDomeCompAt(me.x, me.y); + if (compId == null) { + flashQuizBattleFeedback('ยังไม่ยืนบนโดมคำถาม', false); + return; + } + if (quizBattleAnsweredComps.has(compId)) { + flashQuizBattleFeedback('ตอบโดมนี้ถูกแล้ว', false); + return; + } + const pool = quizBattleMcqPool; + if (!pool.length) { + flashQuizBattleFeedback('ยังไม่มีคำถามในระบบ (Admin → Quiz Battle)', false); + return; + } + const q = pool[(compId - 1) % pool.length]; + if (!q) return; + quizBattleModalCompId = compId; + showQuizBattleMcqModal(q); + } + + function pushSanitizedBattleMcqFromPayload(s) { + if (!s || !Array.isArray(s.battleQuizMcq)) return 0; + let n = 0; + for (let i = 0; i < s.battleQuizMcq.length; i++) { + const raw = s.battleQuizMcq[i]; + if (!raw || !String(raw.text || '').trim()) continue; + const ch = Array.isArray(raw.choices) ? raw.choices : []; + if (ch.length !== 3) continue; + const a = String(ch[0] || '').trim(); + const b = String(ch[1] || '').trim(); + const c = String(ch[2] || '').trim(); + if (!a || !b || !c) continue; + let ci = Number(raw.correctIndex); + if (!Number.isFinite(ci)) ci = 0; + ci = Math.max(0, Math.min(2, Math.floor(ci))); + quizBattleMcqPool.push({ + text: String(raw.text).trim(), + choices: [a, b, c], + correctIndex: ci, + }); + n++; + } + return n; + } + + /** + * โหลดชุดข้อ A/B/C — ลอง Node ก่อน แล้ว fallback PHP อ่านจากดิสก์ (กรณี nginx ไม่ส่ง /api/quiz-settings ถึง Node) + */ + async function loadQuizBattleMcqPool() { + quizBattleMcqPool = []; + const bust = '_=' + Date.now(); + const tryUrls = [ + BASE + '/api/quiz-settings?' + bust, + BASE + '/api-quiz-battle-mcq.php?' + bust, + ]; + for (let u = 0; u < tryUrls.length; u++) { + try { + const r = await fetch(tryUrls[u], { cache: 'no-store' }); + if (!r.ok) continue; + const s = await r.json(); + if (pushSanitizedBattleMcqFromPayload(s) > 0) break; + } catch (e) { /* next url */ } + } + } + + function resetQuizBattlePlayState() { + quizBattleMcqPool = []; + quizBattleAnsweredComps = new Set(); + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + } + + function setupPlayQuizBattleUi() { + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + /* ทดสอบจากเอดิเตอร์: อย่าเปิดแผงคำถามเต็มจอล่าง — บังมองเห็นบอท/แผนที่ (overlay ยังอัปเดตข้อความไว้ถ้าเปิด preview แบบไม่ embed) */ + if (ov) { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else ov.classList.remove('is-hidden'); + } + const phaseEl = document.getElementById('quiz-game-phase-label'); + if (phaseEl) phaseEl.textContent = 'Quiz Battle'; + const qEl = document.getElementById('quiz-game-question'); + if (qEl) qEl.textContent = 'เดินทับโดม (สีฟ้า–ขอบแดง) แล้วกด E เพื่อตอบ A / B / C'; + const tEl = document.getElementById('quiz-game-timer'); + if (tEl) tEl.textContent = ''; + const leg = document.getElementById('quiz-play-legend'); + if (leg) { + leg.textContent = quizBattlePathModeActive(mapData) + ? 'โหมดเส้นทาง: เดินได้เฉพาะช่องเส้นทาง (ม่วงบนแผนที่) — วางโดมบนเส้นทาง · ไม่วาดเส้นทางใน Editor = เดินอิสระ · เปลี่ยนรูปพื้นหลังได้ทีหลัง' + : 'คำถามจาก Admin → Quiz Battle (battleQuizMcq) · ช่องโดมติดกัน = 1 ข้อ · ถูกแล้วได้ +1 ต่อโดม'; + } + quizBattleAnsweredComps = new Set(); + quizBattleModalCompId = null; + quizBattleMcqPool = []; + hideQuizBattleMcqModal(); + playLiveQuizScores = {}; + if (myId != null) playLiveQuizScores[myId] = 0; + others.forEach((_, id) => { playLiveQuizScores[id] = 0; }); + renderPlayQuizScoreboard(playLiveQuizScores); + loadQuizBattleMcqPool().then(() => { + const q2 = document.getElementById('quiz-game-question'); + if (!q2) return; + if (!quizBattleMcqPool.length) { + q2.textContent = 'ยังไม่มีคำถาม — ไปที่ Admin แท็บ Quiz Battle แล้วบันทึกข้อ A/B/C (ครบคำถาม + A + B + C) · หรือเช็คว่าเซิร์ฟเวอร์เกม (Node) รันอยู่ · ลองรีเฟรชแบบ hard refresh (Ctrl+F5)'; + } else { + const q0 = quizBattleMcqPool[0]; + const snippet = q0 && q0.text ? String(q0.text).trim().slice(0, 100) : ''; + const more = q0 && q0.text && String(q0.text).trim().length > 100 ? '…' : ''; + q2.textContent = snippet + ? ('โหลดคำถามแล้ว ' + quizBattleMcqPool.length + ' ข้อ · ตัวอย่าง: 「' + snippet + more + '」\nเข้าโดมสีฟ้า (ขอบแดง) แล้วกด E เพื่อเลือก A / B / C') + : ('โหลดคำถามแล้ว ' + quizBattleMcqPool.length + ' ข้อ — เดินเข้าโดม (ฟ้า–ขอบแดง) แล้วกด E เพื่อตอบ A / B / C'); + } + const ov2 = document.getElementById('quiz-game-overlay'); + if (ov2 && previewMode && editorEmbedReturn) ov2.classList.add('is-hidden'); + }); + } + + function normalizeJumpSurvivePlatformAreaInPlay(md) { + if (!md || md.gameType !== 'jump_survive') return; + const w = md.width || 20, h = md.height || 15; + const src = md.jumpSurvivePlatformArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + rows.push(row); + } + md.jumpSurvivePlatformArea = rows; + } + + function normalizeJumpSurvivePlatformVariantAreaInPlay(md) { + if (!md || md.gameType !== 'jump_survive') return; + const w = md.width || 20, h = md.height || 15; + const pa = md.jumpSurvivePlatformArea || []; + const src = md.jumpSurvivePlatformVariantArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const row = []; + for (let x = 0; x < w; x++) { + const has = pa[y] && pa[y][x] === 1; + let v = has ? Math.floor(Number(src[y] && src[y][x])) : 0; + if (has && (!Number.isFinite(v) || v < 1)) v = 1; + if (v > 3) v = 3; + if (!has) v = 0; + row.push(v); + } + rows.push(row); + } + md.jumpSurvivePlatformVariantArea = rows; + } + + function jumpSurvivePlatformVariantIndexAtPlay(md, tx, ty) { + const pa = md && md.jumpSurvivePlatformArea; + if (!pa || !pa[ty] || pa[ty][tx] !== 1) return 0; + const va = md.jumpSurvivePlatformVariantArea; + let v = va && va[ty] ? Math.floor(Number(va[ty][tx])) : 1; + if (!Number.isFinite(v) || v < 1) v = 1; + if (v > 3) v = 3; + return v; + } + + function normalizeJumpSurviveHazardAreaInPlay(md) { + if (!md || md.gameType !== 'jump_survive') return; + const w = md.width || 20, h = md.height || 15; + const src = md.jumpSurviveHazardArea || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) row.push(r && r[x] === 1 ? 1 : 0); + rows.push(row); + } + md.jumpSurviveHazardArea = rows; + } + + /** โซนตาย jump_survive — กริดคงที่ในโลก (ไม่เลื่อนกับแพลตฟอร์ม) · ช่องที่มีแพลตฟอร์มอยู่ด้วย = ไม่นับ hazard (ยืนบนแพลตฟอร์มชนะ) */ + function jumpSurviveOverlapsHazardArea(md, left, top, right, bottom) { + const ha = md && md.jumpSurviveHazardArea; + if (!ha) return false; + const pa = md && md.jumpSurvivePlatformArea; + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const x0 = Math.max(0, Math.floor(left / ts)); + const x1 = Math.min(w - 1, Math.floor((right - 1e-6) / ts)); + const y0 = Math.max(0, Math.floor(top / ts)); + const y1 = Math.min(h - 1, Math.floor((bottom - 1e-6) / ts)); + for (let ty = y0; ty <= y1; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (!ha[ty] || ha[ty][tx] !== 1) continue; + if (pa && pa[ty] && pa[ty][tx] === 1) continue; + return true; + } + } + return false; + } + + function normalizeShooterSpawnSlotsInPlay(md) { + if (!md || (md.gameType !== 'space_shooter' && md.gameType !== 'jump_survive')) return; + const w = md.width || 20, h = md.height || 15; + const src = md.shooterSpawnSlots || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + row.push(Number.isFinite(n) && n >= 1 && n <= 6 ? Math.floor(n) : 0); + } + rows.push(row); + } + md.shooterSpawnSlots = rows; + } + + function getShooterSpawnWorldCenterFromMap(md, slot1to6, ts) { + if (!md || !md.shooterSpawnSlots) return null; + const w = md.width || 20, h = md.height || 15; + const g = md.shooterSpawnSlots; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (g[y] && g[y][x] === slot1to6) return { cx: (x + 0.5) * ts, cy: (y + 0.5) * ts }; + } + } + return null; + } + + function buildSpaceShooterParticipantRefsPlay() { + const refs = []; + if (me) refs.push({ id: myId, ref: me }); + const humanIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + humanIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + botIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + return refs.filter((r) => r.ref); + } + + function applySpaceShooterSpawnLayoutPlay() { + if (!mapData || mapData.gameType !== 'space_shooter') return; + normalizeShooterSpawnSlotsInPlay(mapData); + const ts = tileSize; + const w = mapData.width || 20, h = mapData.height || 15; + const { cw, ch } = getCharacterFootprintWH(mapData); + const mw = w * ts, mh = h * ts; + const refs = buildSpaceShooterParticipantRefsPlay(); + const n = refs.length || 1; + refs.forEach((entry, idx) => { + const ent = entry.ref; + const slot = (idx % 6) + 1; + const cen = getShooterSpawnWorldCenterFromMap(mapData, slot, ts); + let cx, cy; + if (cen) { + cx = cen.cx; + cy = cen.cy; + } else { + cx = ((idx + 1) / (n + 1)) * mw; + cy = Math.min(mh - ts * 1.2, (h - 1.5) * ts); + } + cx = Math.max(ts * 0.5, Math.min(mw - ts * 0.5, cx)); + cy = clampSpaceShooterWorldCy(cy, mh); + ent.spaceShooterCx = cx; + ent.spaceShooterCy = cy; + ent.spaceShooterSlot = slot; + if (typeof ent.spaceShooterScore !== 'number' || !Number.isFinite(ent.spaceShooterScore)) ent.spaceShooterScore = 0; + ent.x = cx / ts - cw * 0.5; + ent.y = cy / ts - ch * 0.92; + ent.tx = ent.x; + ent.ty = ent.y; + ent.direction = 'up'; + }); + } + + function spaceShooterTimeLimitSecPlay() { + const t = Number(mapData && mapData.spaceShooterTimeSec); + if (Number.isFinite(t) && t === 0) return 0; + if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); + const g = Number(playSpaceShooterMissionTimeSec); + if (Number.isFinite(g) && g > 0 && g < 7200) return Math.floor(g); + return 90; + } + + function spaceShooterRemainingSecPlay() { + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase !== 'live') return null; + const lim = spaceShooterTimeLimitSecPlay(); + if (lim <= 0) return null; + const elapsed = (performance.now() - spaceShooterSessionStartMs) / 1000; + return Math.max(0, Math.ceil(lim - elapsed)); + } + + function endSpaceShooterTimeUp() { + if (spaceShooterGameEnded || !mapData || mapData.gameType !== 'space_shooter') return; + if (isSpaceShooterMissionUiMapPlay()) { + endSpaceShooterMissionRound('time_up'); + return; + } + spaceShooterGameEnded = true; + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterAsteroidExplosions = []; + spaceShooterSpawnAccMs = 0; + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) titleEl.textContent = 'หมดเวลา · Time up'; + msgEl.textContent = 'ยิงยานอวกาศจบแล้ว · SPACE SHOOTER finished — อันดับจากคะแนนยิงหิน'; + listEl.innerHTML = ''; + const ranks = []; + if (myId != null) { + ranks.push({ id: myId, nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', score: Math.max(0, me.spaceShooterScore | 0) }); + } + others.forEach((o, id) => { + ranks.push({ id, nickname: (o && o.nickname) ? String(o.nickname).trim() : id, score: Math.max(0, o.spaceShooterScore | 0) }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && r && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + } + + function syncSpaceShooterFootFromShipCenter(ent) { + if (!ent || !mapData || ent.spaceShooterCx == null || ent.spaceShooterCy == null) return; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ts = tileSize; + ent.x = ent.spaceShooterCx / ts - cw * 0.5; + ent.y = ent.spaceShooterCy / ts - ch * 0.92; + } + + /** จำกัดกลางยานแนวตั้งให้อยู่แค่ครึ่งล่างของแมพ (ไม่ขึ้นเกินกึ่งกลางความสูง) */ + function clampSpaceShooterWorldCy(cy, mhPx) { + if (!Number.isFinite(cy) || !Number.isFinite(mhPx) || mhPx <= 0) return cy; + const edge = 20; + const lo = mhPx * 0.5; + const hi = Math.max(lo + 4, mhPx - edge); + return Math.max(lo, Math.min(hi, cy)); + } + + const SPACE_SHOOTER_ASTEROID_INTERVAL_MIN = 200; + /** ms ระหว่างเกิดอุกาบาต — แมปทับถ้าตั้ง ≥200 */ + function spaceShooterAsteroidSpawnIntervalMsPlay() { + const mapI = Number(mapData && mapData.spaceShooterAsteroidIntervalMs); + if (Number.isFinite(mapI) && mapI >= SPACE_SHOOTER_ASTEROID_INTERVAL_MIN) { + return Math.min(10000, Math.floor(mapI)); + } + const g = Number(playSpaceShooterAsteroidIntervalMs); + if (Number.isFinite(g) && g >= SPACE_SHOOTER_ASTEROID_INTERVAL_MIN) return Math.min(10000, Math.floor(g)); + return 1040; + } + + function stepSpaceShooterPreviewBots(dt) { + if (spaceShooterGameEnded || !playBotsEnabled() || !mapData || mapData.gameType !== 'space_shooter') return; + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase !== 'live') return; + const ts = tileSize; + const mw = (mapData.width || 20) * ts; + const mh = (mapData.height || 15) * ts; + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o || o.spaceShooterCx == null || o.spaceShooterEliminated) return; + let nearest = null, nd = 1e9; + for (let i = 0; i < spaceShooterAsteroids.length; i++) { + const a = spaceShooterAsteroids[i]; + if (!a) continue; + const d = Math.abs(a.x - o.spaceShooterCx) + Math.abs(a.y - o.spaceShooterCy) * 0.15; + if (d < nd) { nd = d; nearest = a; } + } + let vx = 0; + if (nearest) vx = nearest.x > o.spaceShooterCx ? 1 : nearest.x < o.spaceShooterCx ? -1 : 0; + else vx = Math.sin(performance.now() / 900 + bid.length) > 0 ? 1 : -1; + let vy = 0; + if (nearest && Math.abs(nearest.y - o.spaceShooterCy) > ts * 0.25) { + vy = nearest.y > o.spaceShooterCy ? 1 : -1; + } else if (!nearest) { + vy = Math.sin(performance.now() / 700 + bid.charCodeAt(0)) > 0 ? 1 : -1; + } + const spd = (o.botTier === 'sharp' ? 195 : o.botTier === 'weak' ? 115 : 155); + const padX = spaceShooterShipEdgePadWorldPxPlay(); + o.spaceShooterCx = Math.max(padX, Math.min(mw - padX, o.spaceShooterCx + vx * spd * dt)); + o.spaceShooterCy = clampSpaceShooterWorldCy(o.spaceShooterCy + vy * spd * 0.7 * dt, mh); + syncSpaceShooterFootFromShipCenter(o); + o.tx = o.x; + o.ty = o.y; + if (typeof o.spaceShooterBotFireCd !== 'number') o.spaceShooterBotFireCd = 0; + o.spaceShooterBotFireCd -= dt; + if (o.spaceShooterBotFireCd <= 0 && nearest && nd < ts * 5) { + o.spaceShooterBotFireCd = o.botTier === 'sharp' ? 0.22 : o.botTier === 'weak' ? 0.48 : 0.34; + spaceShooterBullets.push({ x: o.spaceShooterCx, y: o.spaceShooterCy - 16, vy: -500, ownerId: bid }); + } + }); + } + + function spaceShooterTickFrame() { + if (!mapData || mapData.gameType !== 'space_shooter') return; + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase !== 'live') return; + const w = mapData.width || 20, h = mapData.height || 15; + const ts = tileSize; + const mw = w * ts, mh = h * ts; + const { cw, ch } = getCharacterFootprintWH(mapData); + const now = performance.now(); + const dt = Math.min(0.055, spaceShooterLastTickMs ? (now - spaceShooterLastTickMs) / 1000 : 0.016); + spaceShooterLastTickMs = now; + tickPlayScrollBg(dt); + spaceShooterTickAsteroidExplosions(dt); + + if (spaceShooterGameEnded) { + const wallClock = Date.now(); + for (let pi = spaceShooterPopups.length - 1; pi >= 0; pi--) { + if (wallClock >= spaceShooterPopups[pi].until) spaceShooterPopups.splice(pi, 1); + } + return; + } + const remSec = spaceShooterRemainingSecPlay(); + if (remSec != null && remSec <= 0) { + endSpaceShooterTimeUp(); + return; + } + + const interval = spaceShooterAsteroidSpawnIntervalMsPlay(); + spaceShooterSpawnAccMs += dt * 1000; + while (spaceShooterSpawnAccMs >= interval) { + spaceShooterSpawnAccMs -= interval; + const maxHp = 1 + Math.floor(Math.random() * SPACE_SHOOTER_ASTEROID_MAX_HP); + const hp0 = maxHp; + spaceShooterAsteroids.push({ + x: 36 + Math.random() * (mw - 72), + y: -45 - Math.random() * 90, + r: spaceShooterAsteroidRadiusFromHpPlay(hp0), + vy: 58 + Math.random() * 62, + maxHp: maxHp, + hp: hp0, + }); + } + + others.forEach((o, id) => { + if (isPreviewBotId(id)) return; + if (o.tx != null) o.x += (o.tx - o.x) * 0.28; + if (o.ty != null) o.y += (o.ty - o.y) * 0.28; + o.spaceShooterCx = (o.x + cw * 0.5) * ts; + o.spaceShooterCy = clampSpaceShooterWorldCy((o.y + ch * 0.92) * ts, mh); + }); + + let vx = 0; + let vy = 0; + if (!isChatFocused()) { + if (keys['ArrowLeft'] || keys['KeyA']) vx -= 1; + if (keys['ArrowRight'] || keys['KeyD']) vx += 1; + if (keys['ArrowUp'] || keys['KeyW']) vy -= 1; + if (keys['ArrowDown'] || keys['KeyS']) vy += 1; + } + const moveSpd = 280; + const canPilotMe = !(isSpaceShooterMissionUiMapPlay() && me.spaceShooterEliminated); + if (canPilotMe) { + if (me.spaceShooterCx != null) { + const padX = spaceShooterShipEdgePadWorldPxPlay(); + me.spaceShooterCx = Math.max(padX, Math.min(mw - padX, me.spaceShooterCx + vx * moveSpd * dt)); + } + if (me.spaceShooterCy != null) { + me.spaceShooterCy = clampSpaceShooterWorldCy(me.spaceShooterCy + vy * moveSpd * dt, mh); + } + syncSpaceShooterFootFromShipCenter(me); + } + + spaceShooterFireCd -= dt; + const autoFireOk = canPilotMe && !isChatFocused(); + if (autoFireOk && spaceShooterFireCd <= 0 && me.spaceShooterCy != null) { + spaceShooterFireCd = 0.21; + spaceShooterBullets.push({ x: me.spaceShooterCx, y: me.spaceShooterCy - 20, vy: -580, ownerId: myId }); + } + + stepSpaceShooterPreviewBots(dt); + + for (let i = spaceShooterBullets.length - 1; i >= 0; i--) { + const b = spaceShooterBullets[i]; + b.y += b.vy * dt; + if (b.y < -50 || b.x < -30 || b.x > mw + 30) spaceShooterBullets.splice(i, 1); + } + for (let ai = spaceShooterAsteroids.length - 1; ai >= 0; ai--) { + const a = spaceShooterAsteroids[ai]; + a.y += a.vy * dt; + if (a.y > mh + 100) spaceShooterAsteroids.splice(ai, 1); + } + + spaceShooterMissionResolveShipAsteroidHitsPlay(); + + for (let bi = spaceShooterBullets.length - 1; bi >= 0; bi--) { + const b = spaceShooterBullets[bi]; + let hit = -1; + for (let ai = 0; ai < spaceShooterAsteroids.length; ai++) { + const a = spaceShooterAsteroids[ai]; + const dx = b.x - a.x, dy = b.y - a.y; + const rr = (a.r + 7) * (a.r + 7); + if (dx * dx + dy * dy <= rr) { hit = ai; break; } + } + if (hit >= 0) { + const a = spaceShooterAsteroids[hit]; + const mh = Math.max(1, Math.floor(Number(a.maxHp)) || SPACE_SHOOTER_ASTEROID_MAX_HP); + a.hp = Math.max(0, Math.floor(Number(a.hp) || mh) - 1); + spaceShooterBullets.splice(bi, 1); + const add = 5; + if (b.ownerId === myId) { + me.spaceShooterScore = Math.max(0, (me.spaceShooterScore || 0) + add); + spaceShooterPopups.push({ x: me.spaceShooterCx, y: me.spaceShooterCy - 30, text: '+5', until: Date.now() + 700 }); + } else { + const o = others.get(b.ownerId); + if (o) { + o.spaceShooterScore = Math.max(0, (o.spaceShooterScore || 0) + add); + spaceShooterPopups.push({ x: o.spaceShooterCx, y: o.spaceShooterCy - 30, text: '+5', until: Date.now() + 700 }); + } + } + if (a.hp <= 0) { + spaceShooterSpawnAsteroidExplosion(a.x, a.y, a.r); + spaceShooterAsteroids.splice(hit, 1); + } else { + a.r = spaceShooterAsteroidRadiusFromHpPlay(a.hp); + } + } + } + + const wallClock = Date.now(); + for (let pi = spaceShooterPopups.length - 1; pi >= 0; pi--) { + /* until ใช้ Date.now() — ห้ามเทียบกับ performance.now() (สเกลคนละระบบ → ลบไม่ออก) */ + if (wallClock >= spaceShooterPopups[pi].until) spaceShooterPopups.splice(pi, 1); + } + + me.direction = 'up'; + me.isWalking = vx !== 0 || vy !== 0; + const tEmit = Date.now(); + if (tEmit - spaceShooterLastMoveEmit > 90 && socket && myId != null) { + spaceShooterLastMoveEmit = tEmit; + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + spaceShooterScore: Math.max(0, me.spaceShooterScore | 0), + }); + } + } + + function drawSpaceShooterCombatLayer(ctx, worldToScreen, zDraw, timeMs) { + if (!mapData || !isSpaceShooter()) return; + const refs = buildSpaceShooterParticipantRefsPlay(); + const wallNow = typeof timeMs === 'number' ? timeMs : Date.now(); + + const astUrls = playSpaceShooterAsteroidSpriteUrls; + + for (let i = 0; i < spaceShooterAsteroids.length; i++) { + const a = spaceShooterAsteroids[i]; + const [sx, sy] = worldToScreen(a.x, a.y); + const sr = Math.max(6, a.r * zDraw); + const si = spaceShooterAsteroidLiveSpriteIndexPlay(a); + let liveUrl = (astUrls && astUrls[si]) ? String(astUrls[si]).trim() : ''; + if (!liveUrl && si > 0 && astUrls && astUrls[0]) liveUrl = String(astUrls[0]).trim(); + const liveRec = liveUrl ? ensureGauntletAssetImage(liveUrl) : null; + const liveAstImg = liveRec && liveRec.ready && liveRec.img && liveRec.img.naturalWidth > 0 ? liveRec.img : null; + if (liveAstImg) { + const iw = liveAstImg.naturalWidth; + const ih = liveAstImg.naturalHeight; + const diam = sr * 2.15; + const sc = Math.min(diam / iw, diam / ih); + const dw = iw * sc; + const dh = ih * sc; + ctx.drawImage(liveAstImg, sx - dw * 0.5, sy - dh * 0.5, dw, dh); + } else { + const grd = ctx.createRadialGradient(sx - sr * 0.25, sy - sr * 0.25, sr * 0.1, sx, sy, sr); + grd.addColorStop(0, 'rgba(90, 85, 95, 0.95)'); + grd.addColorStop(0.55, 'rgba(35, 32, 42, 0.98)'); + grd.addColorStop(1, 'rgba(18, 16, 24, 1)'); + ctx.fillStyle = grd; + ctx.beginPath(); + ctx.arc(sx, sy, sr, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(255, 90, 60, 0.75)'; + ctx.lineWidth = Math.max(1.5, zDraw * 1.2); + ctx.beginPath(); + ctx.arc(sx, sy, sr * 0.88, 0.6, 1.9); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(sx + sr * 0.15, sy + sr * 0.1, sr * 0.35, 0, Math.PI * 2); + ctx.stroke(); + ctx.shadowColor = 'rgba(255, 60, 40, 0.55)'; + ctx.shadowBlur = sr * 0.45; + ctx.strokeStyle = 'rgba(255, 120, 80, 0.35)'; + ctx.beginPath(); + ctx.arc(sx, sy, sr + 3 * zDraw, 0, Math.PI * 2); + ctx.stroke(); + ctx.shadowBlur = 0; + } + } + + if (astUrls && astUrls.length >= 2) { + for (let ei = 0; ei < spaceShooterAsteroidExplosions.length; ei++) { + const ex = spaceShooterAsteroidExplosions[ei]; + const u = astUrls[ex.fi + 1]; + if (!u) continue; + const rec = ensureGauntletAssetImage(u); + const img = rec && rec.ready && rec.img && rec.img.naturalWidth > 0 ? rec.img : null; + if (!img) continue; + const [sx, sy] = worldToScreen(ex.x, ex.y); + const sr = Math.max(6, ex.r * zDraw); + const iw = img.naturalWidth; + const ih = img.naturalHeight; + const diam = sr * 2.4; + const sc = Math.min(diam / iw, diam / ih); + const dw = iw * sc; + const dh = ih * sc; + ctx.drawImage(img, sx - dw * 0.5, sy - dh * 0.5, dw, dh); + } + } + + for (let i = 0; i < spaceShooterBullets.length; i++) { + const b = spaceShooterBullets[i]; + for (let k = 0; k < 4; k++) { + const t = k / 4; + const yy = b.y + t * 22; + const [bx, by] = worldToScreen(b.x, yy); + ctx.fillStyle = `rgba(255, ${180 - k * 35}, ${80 - k * 15}, ${0.95 - k * 0.18})`; + ctx.beginPath(); + ctx.arc(bx, by, Math.max(2, 3.2 * zDraw - k * 0.4), 0, Math.PI * 2); + ctx.fill(); + } + } + + const shipBody = spaceShooterShipBodyScreenPxPlay(zDraw); + const bodyW = shipBody.bodyW; + const bodyH = shipBody.bodyH; + refs.forEach((entry, idx) => { + const ent = entry.ref; + if (ent.spaceShooterCx == null || ent.spaceShooterCy == null) return; + const [sx, sy] = worldToScreen(ent.spaceShooterCx, ent.spaceShooterCy); + const col = SPACE_SHOOTER_SHIP_COLORS[idx % SPACE_SHOOTER_SHIP_COLORS.length]; + const elimShip = !!(isSpaceShooterMissionUiMapPlay() && ent.spaceShooterEliminated); + const slot = Math.max(1, Math.min(6, Number(ent.spaceShooterSlot) || ((idx % 6) + 1))); + const shipUrl = (playSpaceShooterShipImageUrls[slot - 1] || '').trim(); + const shipRec = shipUrl ? ensureGauntletAssetImage(shipUrl) : null; + const shipImg = shipRec && shipRec.ready && shipRec.img && shipRec.img.naturalWidth > 0 ? shipRec.img : null; + let shipDrawW = bodyW * 2.2; + let shipDrawH = bodyH * 2.2; + if (shipImg) { + ctx.save(); + ctx.translate(sx, sy); + if (elimShip) ctx.globalAlpha = 0.38; + const iw = shipImg.naturalWidth; + const ih = shipImg.naturalHeight; + const maxW = bodyW * 2.4; + const maxH = bodyH * 2.4; + const sc = Math.min(maxW / iw, maxH / ih); + const dw = iw * sc; + const dh = ih * sc; + shipDrawW = dw; + shipDrawH = dh; + ctx.drawImage(shipImg, -dw * 0.5, -dh * 0.5, dw, dh); + ctx.restore(); + } else { + ctx.save(); + ctx.translate(sx, sy); + if (elimShip) ctx.globalAlpha = 0.38; + ctx.fillStyle = col; + ctx.strokeStyle = 'rgba(180, 255, 255, 0.85)'; + ctx.lineWidth = Math.max(1.2, zDraw); + ctx.beginPath(); + ctx.moveTo(0, -bodyH * 0.55); + ctx.lineTo(-bodyW * 0.55, bodyH * 0.42); + ctx.lineTo(0, bodyH * 0.22); + ctx.lineTo(bodyW * 0.55, bodyH * 0.42); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + ctx.fillStyle = 'rgba(255, 220, 120, 0.95)'; + ctx.beginPath(); + ctx.moveTo(-bodyW * 0.22, bodyH * 0.38); + ctx.lineTo(0, bodyH * 0.72); + ctx.lineTo(bodyW * 0.22, bodyH * 0.38); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + } + if (isSpaceShooterMissionUiMapPlay()) { + const hits = Math.max(0, Math.min(3, Number(ent.spaceShooterHits) || 0)); + if (hits > 0 && playSpaceShooterShipDamageOverlayUrls && playSpaceShooterShipDamageOverlayUrls.length) { + const oUrl = (playSpaceShooterShipDamageOverlayUrls[hits - 1] || '').trim(); + if (oUrl) { + const oRec = ensureGauntletAssetImage(oUrl); + const oImg = oRec && oRec.ready && oRec.img && oRec.img.naturalWidth > 0 ? oRec.img : null; + if (oImg) { + ctx.save(); + if (elimShip) ctx.globalAlpha = 0.38; + const ow = oImg.naturalWidth; + const oh = oImg.naturalHeight; + const scO = Math.min(shipDrawW / ow, shipDrawH / oh); + const odw = ow * scO; + const odh = oh * scO; + ctx.drawImage(oImg, sx - odw * 0.5, sy - odh * 0.5, odw, odh); + ctx.restore(); + } + } + } + } + const name = (ent.nickname || '').slice(0, 10); + if (name) { + ctx.font = `${Math.max(9, 10 * zDraw)}px system-ui, "Kanit", sans-serif`; + ctx.textAlign = 'center'; + ctx.fillStyle = 'rgba(224, 242, 255, 0.92)'; + ctx.strokeStyle = 'rgba(0, 20, 40, 0.85)'; + ctx.lineWidth = 3; + ctx.strokeText(name, sx, sy - bodyH * 0.62); + ctx.fillText(name, sx, sy - bodyH * 0.62); + ctx.textAlign = 'left'; + } + }); + + for (let i = 0; i < spaceShooterPopups.length; i++) { + const p = spaceShooterPopups[i]; + if (wallNow >= p.until) continue; + const [px, py] = worldToScreen(p.x, p.y); + const dur = 700; + const age = 1 - Math.max(0, Math.min(1, (p.until - wallNow) / dur)); + ctx.save(); + ctx.globalAlpha = Math.max(0, 1 - age * 0.92); + ctx.font = `bold ${Math.max(12, 16 * zDraw)}px Orbitron, ui-sans-serif, sans-serif`; + ctx.textAlign = 'center'; + ctx.fillStyle = '#ffe066'; + ctx.strokeStyle = 'rgba(120, 60, 0, 0.6)'; + ctx.lineWidth = 3; + ctx.strokeText(p.text || '+5', px, py - age * 18); + ctx.fillText(p.text || '+5', px, py - age * 18); + ctx.restore(); + } + } + + const BALLOON_BOSS_PLAYER_COLORS = ['#ff79c6', '#7aa2f7', '#f9e2af', '#f7768e', '#9ece6a', '#bb9af7']; + + function normalizeBalloonBossPlayerSlotsInPlay(md) { + if (!md || md.gameType !== 'balloon_boss') return; + const w = md.width || 20, h = md.height || 15; + const src = md.balloonBossPlayerSlots || []; + const rows = []; + for (let y = 0; y < h; y++) { + const r = src[y]; + const row = []; + for (let x = 0; x < w; x++) { + const v = r && r[x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + row.push(Number.isFinite(n) && n >= 1 && n <= 6 ? Math.floor(n) : 0); + } + rows.push(row); + } + md.balloonBossPlayerSlots = rows; + } + + function getBalloonBossPlayerSpawnWorldCenterFromMap(md, slot1to6, ts) { + if (!md || !md.balloonBossPlayerSlots) return null; + const w = md.width || 20, h = md.height || 15; + const g = md.balloonBossPlayerSlots; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (g[y] && g[y][x] === slot1to6) return { cx: (x + 0.5) * ts, cy: (y + 0.5) * ts }; + } + } + return null; + } + + function getBalloonBossBossWorldCenterPlay(md, ts) { + const bx = md && md.balloonBossBossSpawn; + if (bx && Number.isFinite(Number(bx.x)) && Number.isFinite(Number(bx.y))) { + return { cx: (Number(bx.x) + 0.5) * ts, cy: (Number(bx.y) + 0.5) * ts }; + } + const w = md.width || 20, h = md.height || 15; + return { cx: (w * 0.5) * ts, cy: (h * 0.38) * ts }; + } + + /** + * จุดกลางบอสระหว่างเล่น — ลอยแบบลูกโป่งจากผลรวม sine (deterministic ต่อเวลาในรอบ) + * เพื่อให้ทุก client ได้ตำแหน่งใกล้เคียงกัน (ไม่ใช้ state สุ่มต่อเครื่อง) + */ + function getBalloonBossBossLiveCenterPlay(md, ts) { + if (!md || md.gameType !== 'balloon_boss') return getBalloonBossBossWorldCenterPlay(md, ts); + const base = getBalloonBossBossWorldCenterPlay(md, ts); + const w = md.width || 20, h = md.height || 15; + const mw = w * ts, mh = h * ts; + const tSec = balloonBossSessionStartMs > 0 + ? (performance.now() - balloonBossSessionStartMs) / 1000 + : performance.now() / 1000; + const ax = Math.min(mw * 0.19, 240); + const ay = Math.min(mh * 0.17, 200); + const cx = base.cx + Math.sin(tSec * 0.33 + 0.75) * ax + Math.sin(tSec * 0.69 + 0.15) * (ax * 0.4); + const cy = base.cy + Math.cos(tSec * 0.29 + 1.05) * ay + Math.cos(tSec * 0.64 + 0.45) * (ay * 0.37); + return clampBalloonBossWorld(cx, cy, mw, mh); + } + + /** จุดโลกประมาณกลางกระจุกลูกโป่ง (ใช้ hit จากกระสุนบอส) — ไม่ใช่จุดเท้า */ + function balloonBossBalloonClusterWorldPlay(ent, bossCx, bossCy) { + if (!ent || ent.balloonBossCx == null) return { cx: 0, cy: 0 }; + const dx = ent.balloonBossCx - bossCx; + const dy = ent.balloonBossCy - bossCy; + const L = Math.hypot(dx, dy) || 1; + const ox = (dx / L) * 22; + const oy = (dy / L) * 12; + return { + cx: ent.balloonBossCx + ox, + cy: ent.balloonBossCy - 52 + oy * 0.35, + }; + } + + /** ผู้เล่นหลักเด้งกับวงชนบอส (บอสเป็นวงนิ่งใน world) */ + function balloonBossPlayerBossElasticPlay(meEnt, bossCx, bossCy, bossCollideR, mw, mh) { + if (!meEnt || meEnt.balloonBossEliminated || meEnt.balloonBossCx == null) return; + const rMe = 44; + const rB = Math.max(30, bossCollideR * 0.9); + const dx = meEnt.balloonBossCx - bossCx; + const dy = meEnt.balloonBossCy - bossCy; + const dist = Math.hypot(dx, dy) || 1e-6; + const minD = rMe + rB; + if (dist >= minD) return; + const nx = dx / dist; + const ny = dy / dist; + const push = (minD - dist) * 0.58; + meEnt.balloonBossCx += nx * push; + meEnt.balloonBossCy += ny * push; + const vn = meEnt.balloonBossVelX * nx + meEnt.balloonBossVelY * ny; + if (vn < 0) { + meEnt.balloonBossVelX -= 1.85 * vn * nx; + meEnt.balloonBossVelY -= 1.85 * vn * ny; + } + const cl = clampBalloonBossWorld(meEnt.balloonBossCx, meEnt.balloonBossCy, mw, mh); + meEnt.balloonBossCx = cl.cx; + meEnt.balloonBossCy = cl.cy; + } + + /** ผู้เล่นหลักเด้งกับคนอื่น (บอทขยับรับแรงปะทะเล็กน้อย) */ + function balloonBossPlayerElasticCollisionsPlay(meEnt, aliveRefs, mw, mh) { + if (!meEnt || meEnt.balloonBossEliminated || meEnt.balloonBossCx == null) return; + const rMe = 44; + const rO = 44; + const minD = rMe + rO - 2; + for (let ii = 0; ii < aliveRefs.length; ii++) { + const entry = aliveRefs[ii]; + if (!entry || entry.ref === meEnt) continue; + const o = entry.ref; + if (!o || o.balloonBossEliminated || o.balloonBossCx == null) continue; + const dx = meEnt.balloonBossCx - o.balloonBossCx; + const dy = meEnt.balloonBossCy - o.balloonBossCy; + const dist = Math.hypot(dx, dy); + if (!dist || dist >= minD) continue; + const nx = dx / dist; + const ny = dy / dist; + const push = (minD - dist) * 0.52; + meEnt.balloonBossCx += nx * push; + meEnt.balloonBossCy += ny * push; + const vn = meEnt.balloonBossVelX * nx + meEnt.balloonBossVelY * ny; + if (vn < 0) { + meEnt.balloonBossVelX -= 1.85 * vn * nx; + meEnt.balloonBossVelY -= 1.85 * vn * ny; + } + if (isPreviewBotId(entry.id)) { + const pushO = (minD - dist) * 0.48; + o.balloonBossCx -= nx * pushO; + o.balloonBossCy -= ny * pushO; + } + } + const cl2 = clampBalloonBossWorld(meEnt.balloonBossCx, meEnt.balloonBossCy, mw, mh); + meEnt.balloonBossCx = cl2.cx; + meEnt.balloonBossCy = cl2.cy; + } + + /** หมายเลขสล็อต 1–6 ที่มีบนแมป (เรียง) — ถ้ามีแค่ 3 ช่อง ผู้เล่นจะวนที่นั่ง 3 จุดรอบบอส · Occupied P-slots on map for spawn cycling */ + function getBalloonBossOccupiedSlotNumbersPlay(md) { + if (!md || !md.balloonBossPlayerSlots) return []; + const g = md.balloonBossPlayerSlots; + const w = md.width || 20, h = md.height || 15; + const seen = new Set(); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const v = g[y] && g[y][x]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + if (Number.isFinite(n) && n >= 1 && n <= 6) seen.add(Math.floor(n)); + } + } + return Array.from(seen).sort((a, b) => a - b); + } + + function buildBalloonBossParticipantRefsPlay() { + const refs = []; + if (me) refs.push({ id: myId, ref: me }); + const humanIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + humanIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + botIds.forEach((id) => { + const o = others.get(id); + if (o) refs.push({ id, ref: o }); + }); + return refs.filter((r) => r.ref); + } + + function balloonBossBalloonsStartPlay() { + const n = Number(mapData && mapData.balloonBossBalloonsPerPlayer); + /** ไม่มีค่าในแมป = 3 ลูก (Mega Virus mock) — ไม่ใช้ 5 เพราะบอท/preview ติด 5 */ + return Math.max(1, Math.min(12, Number.isFinite(n) ? Math.floor(n) : 3)); + } + + function balloonBossMaxHpPlay() { + const n = Number(mapData && mapData.balloonBossMaxHp); + return Math.max(20, Math.min(9999, Number.isFinite(n) ? Math.floor(n) : 100)); + } + + function balloonBossFireDelayMsPlay() { + const n = Number(mapData && mapData.balloonBossFireDelayMs); + return Math.max(120, Math.min(2000, Number.isFinite(n) ? Math.floor(n) : 380)); + } + + /** ความเร่งยาน (px/s²) — ต่ำ = คุมยาก ลื่น */ + function balloonBossShipAccelPlay() { + const n = Number(mapData && mapData.balloonBossShipAccel); + return Math.max(80, Math.min(520, Number.isFinite(n) ? n : 210)); + } + + /** ความเร็วสูงสุดยาน (px/s) */ + function balloonBossShipMaxSpeedPlay() { + const n = Number(mapData && mapData.balloonBossShipMaxSpeed); + return Math.max(60, Math.min(320, Number.isFinite(n) ? n : 158)); + } + + /** แรงหน่วงต่อวินาที (velocity *= 1 - min(1, drag*dt)) — สูง = หยุดเร็วขึ้น */ + function balloonBossShipDragPlay() { + const n = Number(mapData && mapData.balloonBossShipDrag); + return Math.max(0.4, Math.min(6, Number.isFinite(n) ? n : 1.65)); + } + + function balloonBossTimeLimitSecPlay() { + const t = Number(mapData && mapData.balloonBossTimeSec); + if (Number.isFinite(t) && t === 0) return 0; + if (Number.isFinite(t) && t > 0 && t < 7200) return Math.floor(t); + const g = Number(playBalloonBossMissionTimeSec); + if (Number.isFinite(g) && g > 0 && g < 7200) return Math.floor(g); + return 120; + } + + function balloonBossRemainingSecPlay() { + const lim = balloonBossTimeLimitSecPlay(); + if (lim <= 0) return null; + if (isMegaVirusMissionShellMapPlay() && isGauntletCrownPregameBlockingPlay()) return lim; + if (!balloonBossSessionStartMs) return lim; + const elapsed = (performance.now() - balloonBossSessionStartMs) / 1000; + return Math.max(0, Math.ceil(lim - elapsed)); + } + + function balloonBossTeamDamagePlay() { + let s = Math.max(0, me.balloonBossScore | 0); + others.forEach((o) => { s += Math.max(0, o.balloonBossScore | 0); }); + return s; + } + + /** รวมดาเมจที่ทำกับบอส (ไม่เท่ากับคะแนน — คะแนน +10 ต่อครั้ง, HP บอส -2 ต่อครั้ง) */ + function balloonBossTeamBossDamagePlay() { + let s = Math.max(0, me.balloonBossBossDmg | 0); + others.forEach((o) => { s += Math.max(0, o.balloonBossBossDmg | 0); }); + return s; + } + + function syncBalloonBossFootFromCenter(ent) { + if (!ent || !mapData || ent.balloonBossCx == null || ent.balloonBossCy == null) return; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ts = tileSize; + ent.x = ent.balloonBossCx / ts - cw * 0.5; + ent.y = ent.balloonBossCy / ts - ch * 0.92; + } + + function clampBalloonBossWorld(cx, cy, mw, mh) { + const e = 22; + return { + cx: Math.max(e, Math.min(mw - e, cx)), + cy: Math.max(e, Math.min(mh - e, cy)), + }; + } + + function applyBalloonBossSpawnLayoutPlay() { + if (!mapData || mapData.gameType !== 'balloon_boss') return; + normalizeBalloonBossPlayerSlotsInPlay(mapData); + const ts = tileSize; + const w = mapData.width || 20, h = mapData.height || 15; + const { cw, ch } = getCharacterFootprintWH(mapData); + const mw = w * ts, mh = h * ts; + const refs = buildBalloonBossParticipantRefsPlay(); + const n = refs.length || 1; + const bStart = balloonBossBalloonsStartPlay(); + const slotCycle = getBalloonBossOccupiedSlotNumbersPlay(mapData); + refs.forEach((entry, idx) => { + const ent = entry.ref; + const slot = slotCycle.length ? slotCycle[idx % slotCycle.length] : ((idx % 6) + 1); + const cen = getBalloonBossPlayerSpawnWorldCenterFromMap(mapData, slot, ts); + let cx, cy; + if (cen) { + cx = cen.cx; + cy = cen.cy; + } else { + const t = (idx + 1) / (n + 1); + cx = t * mw; + cy = Math.min(mh - ts * 1.2, (h - 1.2) * ts); + } + const cl = clampBalloonBossWorld(cx, cy, mw, mh); + ent.balloonBossSkinSlot = slot; + ent.balloonBossCx = cl.cx; + ent.balloonBossCy = cl.cy; + if (typeof ent.balloonBossScore !== 'number' || !Number.isFinite(ent.balloonBossScore)) ent.balloonBossScore = 0; + if (typeof ent.balloonBossBossDmg !== 'number' || !Number.isFinite(ent.balloonBossBossDmg)) ent.balloonBossBossDmg = 0; + /** จำนวนลูกโป่งตามแมปเสมอเมื่อจัดตำแหน่ง — ไม่ค้าง 5 จาก preview bot · Always sync start count */ + ent.balloonBossBalloons = bStart; + if (typeof ent.balloonBossHitIframe !== 'number') ent.balloonBossHitIframe = 0; + ent.balloonBossVelX = 0; + ent.balloonBossVelY = 0; + ent.x = ent.balloonBossCx / ts - cw * 0.5; + ent.y = ent.balloonBossCy / ts - ch * 0.92; + ent.tx = ent.x; + ent.ty = ent.y; + ent.direction = 'down'; + if (typeof ent.balloonBossAimRad !== 'number' || !Number.isFinite(ent.balloonBossAimRad)) { + ent.balloonBossAimRad = Math.random() * Math.PI * 2; + } + }); + } + + function endBalloonBossGame(reason) { + if (balloonBossGameEnded || !mapData || mapData.gameType !== 'balloon_boss') return; + balloonBossGameEnded = true; + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + balloonBossScorePopups = []; + if (isMegaVirusMissionShellMapPlay()) { + applyMegaVirusMissionPanelImages(); + beginBalloonBossMegaVirusResultFlashSequenceThenGcm(balloonBossBuildMissionPayloadPlay(reason), reason); + return; + } + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) { + titleEl.textContent = reason === 'victory' ? 'ชนะบอส! · Boss defeated' : 'หมดเวลา · Time up'; + } + msgEl.textContent = reason === 'victory' + ? 'MEGA VIRUS ถูกกำจัดแล้ว — อันดับจากคะแนนการยิงโดน' + : 'หมดเวลา — อันดับจากคะแนนการยิงโดน'; + listEl.innerHTML = ''; + const ranks = []; + if (myId != null) { + ranks.push({ id: myId, nickname: (me.nickname || nick || 'คุณ').trim() || 'คุณ', score: Math.max(0, me.balloonBossScore | 0) }); + } + others.forEach((o, id) => { + ranks.push({ id, nickname: (o && o.nickname) ? String(o.nickname).trim() : id, score: Math.max(0, o.balloonBossScore | 0) }); + }); + ranks.sort((a, b) => b.score - a.score || String(a.nickname).localeCompare(String(b.nickname), 'th')); + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && r && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)} pts`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + } + + function stepBalloonBossPreviewBots(dt, mw, mh, bossCx, bossCy, ts) { + if (!playBotsEnabled() || !mapData || mapData.gameType !== 'balloon_boss') return; + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const o = others.get(bid); + if (!o || o.balloonBossEliminated || o.balloonBossCx == null) return; + const wob = Math.sin(performance.now() / 800 + bid.length) * 0.55; + const wob2 = Math.cos(performance.now() / 1100 + bid.charCodeAt(0)) * 0.45; + const spd = 48; + let nx = o.balloonBossCx + wob * spd * dt; + let ny = o.balloonBossCy + wob2 * spd * 0.72 * dt; + const cl = clampBalloonBossWorld(nx, ny, mw, mh); + o.balloonBossCx = cl.cx; + o.balloonBossCy = cl.cy; + syncBalloonBossFootFromCenter(o); + o.direction = 'down'; + o.tx = o.x; + o.ty = o.y; + if (typeof o.balloonBossBotNextFire === 'number') o.balloonBossBotNextFire -= dt; + else o.balloonBossBotNextFire = 0.8 + Math.random() * 0.9; + if (o.balloonBossBotNextFire <= 0) { + o.balloonBossBotNextFire = 0.9 + Math.random() * 1.1; + const delayMs = balloonBossFireDelayMsPlay(); + const dx = bossCx - o.balloonBossCx; + const dy = bossCy - o.balloonBossCy; + const L = Math.hypot(dx, dy) || 1; + const bspd = 500 + Math.random() * 90; + balloonBossPendingShots.push({ + releaseAt: performance.now() + delayMs, + sx: o.balloonBossCx, sy: o.balloonBossCy, + vx: (dx / L) * bspd, + vy: (dy / L) * bspd, + ownerId: bid, + }); + } + }); + } + + function balloonBossTickFrame() { + if (!mapData || mapData.gameType !== 'balloon_boss') return; + if (isMegaVirusMissionShellMapPlay() && isGauntletCrownPregameBlockingPlay()) return; + const w = mapData.width || 20, h = mapData.height || 15; + const ts = tileSize; + const mw = w * ts, mh = h * ts; + const { cw, ch } = getCharacterFootprintWH(mapData); + others.forEach((o, id) => { + if (isPreviewBotId(id)) return; + if (!o || o.balloonBossEliminated) return; + if (o.tx != null) o.x += (o.tx - o.x) * 0.24; + if (o.ty != null) o.y += (o.ty - o.y) * 0.24; + const cx = (o.x + cw * 0.5) * ts; + const cy = (o.y + ch * 0.92) * ts; + const cl = clampBalloonBossWorld(cx, cy, mw, mh); + o.balloonBossCx = cl.cx; + o.balloonBossCy = cl.cy; + }); + const now = performance.now(); + const dt = Math.min(0.055, balloonBossLastTickMs ? (now - balloonBossLastTickMs) / 1000 : 0.016); + balloonBossLastTickMs = now; + const bossC = getBalloonBossBossLiveCenterPlay(mapData, ts); + const bossR = Math.max(36, ts * 1.15); + const maxHp = balloonBossMaxHpPlay(); + const teamBossDmg = balloonBossTeamBossDamagePlay(); + + if (me.balloonBossEliminated) { + me.balloonBossVelX = 0; + me.balloonBossVelY = 0; + } + if (balloonBossGameEnded) { + for (let hi = balloonBossHitFx.length - 1; hi >= 0; hi--) { + balloonBossHitFx[hi].t -= dt; + if (balloonBossHitFx[hi].t <= 0) balloonBossHitFx.splice(hi, 1); + } + for (let si = balloonBossScorePopups.length - 1; si >= 0; si--) { + const sp = balloonBossScorePopups[si]; + sp.t -= dt; + if (sp.t <= 0) balloonBossScorePopups.splice(si, 1); + } + return; + } + const remSec = balloonBossRemainingSecPlay(); + if (remSec != null && remSec <= 0) { + endBalloonBossGame('time'); + return; + } + if (teamBossDmg >= maxHp) { + endBalloonBossGame('victory'); + return; + } + const allRefsEarly = buildBalloonBossParticipantRefsPlay(); + if (allRefsEarly.length > 0 && allRefsEarly.every((e) => e.ref && e.ref.balloonBossEliminated)) { + endBalloonBossGame('all_dead'); + return; + } + + for (let hi = balloonBossHitFx.length - 1; hi >= 0; hi--) { + balloonBossHitFx[hi].t -= dt; + if (balloonBossHitFx[hi].t <= 0) balloonBossHitFx.splice(hi, 1); + } + for (let si = balloonBossScorePopups.length - 1; si >= 0; si--) { + const sp = balloonBossScorePopups[si]; + sp.t -= dt; + if (sp.t <= 0) balloonBossScorePopups.splice(si, 1); + } + + const aliveRefs = allRefsEarly.filter((e) => e.ref && !e.ref.balloonBossEliminated); + + for (let pi = balloonBossPendingShots.length - 1; pi >= 0; pi--) { + const pnd = balloonBossPendingShots[pi]; + if (now >= pnd.releaseAt) { + balloonBossPlayerBullets.push({ x: pnd.sx, y: pnd.sy, vx: pnd.vx, vy: pnd.vy, ownerId: pnd.ownerId }); + balloonBossPendingShots.splice(pi, 1); + } + } + + const bossFireEvery = 1.42; + balloonBossBossFireAcc += dt; + while (balloonBossBossFireAcc >= bossFireEvery) { + balloonBossBossFireAcc -= bossFireEvery; + if (aliveRefs.length) { + const ang = Math.random() * Math.PI * 2; + const spd = 88 + Math.random() * 72; + balloonBossBossBullets.push({ + x: bossC.cx, y: bossC.cy, + vx: Math.cos(ang) * spd, vy: Math.sin(ang) * spd, + gy: 48 + Math.random() * 56, + }); + } + } + + function applyBalloonBossHitToEntity(ent) { + if (!ent || ent.balloonBossEliminated) return; + if (ent.balloonBossHitIframe > now) return; + ent.balloonBossHitIframe = now + 520; + ent.balloonBossBalloons = Math.max(0, (ent.balloonBossBalloons | 0) - 1); + if (ent.balloonBossBalloons <= 0) { + ent.balloonBossEliminated = true; + ent.balloonBossBalloons = 0; + } + if (ent === me && socket) { + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + balloonBossBalloons: me.balloonBossBalloons, + balloonBossEliminated: !!me.balloonBossEliminated, + balloonBossScore: Math.max(0, me.balloonBossScore | 0), + balloonBossBossDmg: Math.max(0, me.balloonBossBossDmg | 0), + }); + } + } + + for (let bi = balloonBossBossBullets.length - 1; bi >= 0; bi--) { + const b = balloonBossBossBullets[bi]; + const gy = typeof b.gy === 'number' && Number.isFinite(b.gy) ? b.gy : 62; + b.vy += gy * dt; + b.x += b.vx * dt; + b.y += b.vy * dt; + if (b.x < -80 || b.x > mw + 80 || b.y < -80 || b.y > mh + 80) { + balloonBossBossBullets.splice(bi, 1); + continue; + } + let hit = false; + aliveRefs.forEach((entry) => { + if (hit) return; + const ent = entry.ref; + if (!ent || ent.balloonBossCx == null) return; + const cc = balloonBossBalloonClusterWorldPlay(ent, bossC.cx, bossC.cy); + const dx = b.x - cc.cx, dy = b.y - cc.cy; + if (dx * dx + dy * dy <= (36 * 36)) { + hit = true; + applyBalloonBossHitToEntity(ent); + } + }); + if (hit) balloonBossBossBullets.splice(bi, 1); + } + + for (let i = balloonBossPlayerBullets.length - 1; i >= 0; i--) { + const b = balloonBossPlayerBullets[i]; + b.x += b.vx * dt; + b.y += b.vy * dt; + if (b.x < -60 || b.x > mw + 60 || b.y < -60 || b.y > mh + 60) { + balloonBossPlayerBullets.splice(i, 1); + continue; + } + let pHit = false; + for (let ji = 0; ji < aliveRefs.length; ji++) { + if (pHit) break; + const entry = aliveRefs[ji]; + const ent = entry.ref; + if (!ent || ent.balloonBossCx == null) continue; + if (String(entry.id) === String(b.ownerId)) continue; + const canBalloonHit = isPreviewBotId(entry.id) || ent === me; + if (!canBalloonHit) continue; + const cc = balloonBossBalloonClusterWorldPlay(ent, bossC.cx, bossC.cy); + const pdx = b.x - cc.cx, pdy = b.y - cc.cy; + if (pdx * pdx + pdy * pdy <= (34 * 34)) { + applyBalloonBossHitToEntity(ent); + balloonBossPlayerBullets.splice(i, 1); + pHit = true; + } + } + if (pHit) continue; + const dx = b.x - bossC.cx, dy = b.y - bossC.cy; + if (dx * dx + dy * dy <= bossR * bossR) { + balloonBossHitFx.push({ x: bossC.cx, y: bossC.cy, t: 0.22 }); + balloonBossScorePopups.push({ + ownerId: b.ownerId, + x: b.x, + y: b.y, + t: 0.85, + tMax: 0.85, + }); + const addScore = 10; + const addBossDmg = 2; + if (b.ownerId === myId) { + me.balloonBossScore = Math.max(0, (me.balloonBossScore | 0) + addScore); + me.balloonBossBossDmg = Math.max(0, (me.balloonBossBossDmg | 0) + addBossDmg); + if (socket && myId != null) { + balloonBossLastMoveEmit = Date.now(); + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + balloonBossScore: Math.max(0, me.balloonBossScore | 0), + balloonBossBossDmg: Math.max(0, me.balloonBossBossDmg | 0), + balloonBossBalloons: Math.max(0, me.balloonBossBalloons | 0), + balloonBossEliminated: !!me.balloonBossEliminated, + }); + } + } else { + const o = others.get(b.ownerId); + if (o) { + o.balloonBossScore = Math.max(0, (o.balloonBossScore | 0) + addScore); + o.balloonBossBossDmg = Math.max(0, (o.balloonBossBossDmg | 0) + addBossDmg); + } + } + balloonBossPlayerBullets.splice(i, 1); + } + } + + const tWind = performance.now() * 0.001; + const gustAx = Math.sin(tWind * 0.36 + 1.15) * 62 + Math.sin(tWind * 1.05) * 24; + const gustAy = Math.cos(tWind * 0.39 + 0.28) * 58 + Math.cos(tWind * 0.93) * 22; + const maxSpd = Math.min(228, balloonBossShipMaxSpeedPlay() * 1.32); + const dragPerSec = balloonBossShipDragPlay() * 0.4; + if (typeof me.balloonBossVelX !== 'number' || !Number.isFinite(me.balloonBossVelX)) me.balloonBossVelX = 0; + if (typeof me.balloonBossVelY !== 'number' || !Number.isFinite(me.balloonBossVelY)) me.balloonBossVelY = 0; + let nvx = me.balloonBossVelX; + let nvy = me.balloonBossVelY; + nvx += gustAx * dt; + nvy += gustAy * dt; + const drag = Math.min(1, dragPerSec * dt); + nvx *= (1 - drag); + nvy *= (1 - drag); + const curSpd = Math.sqrt(nvx * nvx + nvy * nvy); + if (curSpd > maxSpd) { + const s = maxSpd / curSpd; + nvx *= s; + nvy *= s; + } + me.balloonBossVelX = nvx; + me.balloonBossVelY = nvy; + if (!me.balloonBossEliminated && me.balloonBossCx != null) { + const ncx = me.balloonBossCx + nvx * dt; + const ncy = me.balloonBossCy + nvy * dt; + const cl = clampBalloonBossWorld(ncx, ncy, mw, mh); + if (Math.abs(cl.cx - ncx) > 0.5) me.balloonBossVelX *= -0.88; + if (Math.abs(cl.cy - ncy) > 0.5) me.balloonBossVelY *= -0.88; + me.balloonBossCx = cl.cx; + me.balloonBossCy = cl.cy; + } + syncBalloonBossFootFromCenter(me); + balloonBossPlayerElasticCollisionsPlay(me, aliveRefs, mw, mh); + balloonBossPlayerBossElasticPlay(me, bossC.cx, bossC.cy, bossR, mw, mh); + syncBalloonBossFootFromCenter(me); + /** ฉากนี้แสดงตัวหันหน้าเข้ากล้อง — sync กับ combat draw */ + me.direction = 'down'; + me.isWalking = Math.abs(me.balloonBossVelX) > 12 || Math.abs(me.balloonBossVelY) > 12; + + /** วงลูกศรหมุนเองเรื่อย ๆ (คุมไม่ได้) — Mega Virus ยิงตามมุมลูกศร ณ ตอนกด Space */ + aliveRefs.forEach((e) => { + const r = e.ref; + if (!r || r.balloonBossEliminated) return; + if (typeof r.balloonBossAimRad !== 'number' || !Number.isFinite(r.balloonBossAimRad)) { + r.balloonBossAimRad = Math.random() * Math.PI * 2; + } + r.balloonBossAimRad += 1.02 * dt; + }); + + balloonBossPlayerFireCd -= dt; + const wantFire = !isChatFocused() && !!keys['Space'] && !me.balloonBossEliminated; + if (wantFire && balloonBossPlayerFireCd <= 0 && me.balloonBossCy != null) { + balloonBossPlayerFireCd = 0.35; + const delayMs = balloonBossFireDelayMsPlay(); + const bspd = 480; + let vx; + let vy; + if (isMegaVirusMissionShellMapPlay()) { + const aim = (typeof me.balloonBossAimRad === 'number' && Number.isFinite(me.balloonBossAimRad)) + ? me.balloonBossAimRad + : Math.atan2(bossC.cy - me.balloonBossCy, bossC.cx - me.balloonBossCx); + vx = Math.cos(aim) * bspd; + vy = Math.sin(aim) * bspd; + } else { + const dx = bossC.cx - me.balloonBossCx; + const dy = bossC.cy - me.balloonBossCy; + const L = Math.hypot(dx, dy) || 1; + vx = (dx / L) * bspd; + vy = (dy / L) * bspd; + } + balloonBossPendingShots.push({ + releaseAt: now + delayMs, + sx: me.balloonBossCx, sy: me.balloonBossCy, + vx, + vy, + ownerId: myId, + }); + } + + stepBalloonBossPreviewBots(dt, mw, mh, bossC.cx, bossC.cy, ts); + + const tEmit = Date.now(); + if (tEmit - balloonBossLastMoveEmit > 95 && socket && myId != null) { + balloonBossLastMoveEmit = tEmit; + socket.emit('move', { + x: me.x, y: me.y, direction: me.direction, + balloonBossScore: Math.max(0, me.balloonBossScore | 0), + balloonBossBossDmg: Math.max(0, me.balloonBossBossDmg | 0), + balloonBossBalloons: Math.max(0, me.balloonBossBalloons | 0), + balloonBossEliminated: !!me.balloonBossEliminated, + }); + } + } + + function drawBalloonBossCombatLayer(ctx, worldToScreen, zDraw, timeMs) { + if (!mapData || !isBalloonBoss()) return; + const ts = tileSize; + const w = mapData.width || 20, h = mapData.height || 15; + const mw = w * ts, mh = h * ts; + const bossC = getBalloonBossBossLiveCenterPlay(mapData, ts); + const maxHp = balloonBossMaxHpPlay(); + const dmg = balloonBossTeamBossDamagePlay(); + const hpLeft = Math.max(0, maxHp - dmg); + const bossR = Math.max(36, ts * 1.15); + const refs = buildBalloonBossParticipantRefsPlay(); + const leaderId = (() => { + let best = null, bs = -1; + refs.forEach((e) => { + const sc = Math.max(0, e.ref.balloonBossScore | 0); + if (sc > bs) { bs = sc; best = e.id; } + }); + return best; + })(); + const bossImgUrl = normalizeGauntletAssetUrlForPlay(String(playBalloonBossBossImageUrl || '')); + const bossImgRec = bossImgUrl ? ensureGauntletAssetImage(bossImgUrl) : null; + + for (let hi = 0; hi < balloonBossHitFx.length; hi++) { + const fx = balloonBossHitFx[hi]; + const [hx, hy] = worldToScreen(fx.x, fx.y); + const pulse = 1 + (fx.t || 0) * 3; + ctx.save(); + ctx.globalAlpha = Math.min(1, (fx.t || 0) * 4); + ctx.strokeStyle = 'rgba(255, 220, 120, 0.9)'; + ctx.lineWidth = 3 * zDraw; + ctx.beginPath(); + ctx.arc(hx, hy, bossR * zDraw * 0.5 * pulse, 0, Math.PI * 2); + ctx.stroke(); + ctx.restore(); + } + + const score10PopUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/score+10.png'); + const score10PopRec = score10PopUrl ? ensureGauntletAssetImage(score10PopUrl) : null; + for (let si = 0; si < balloonBossScorePopups.length; si++) { + const sp = balloonBossScorePopups[si]; + let wx = sp.x; + let wy = sp.y; + if (sp.ownerId != null) { + const oid = sp.ownerId; + const ent = (myId != null && String(oid) === String(myId)) ? me : others.get(oid); + if (ent && ent.balloonBossCx != null && !ent.balloonBossEliminated) { + const cc = balloonBossBalloonClusterWorldPlay(ent, bossC.cx, bossC.cy); + const tMax = typeof sp.tMax === 'number' && sp.tMax > 0 ? sp.tMax : 0.95; + const rise = ((tMax - Math.max(0, sp.t || 0)) / tMax) * 58; + wx = cc.cx; + wy = cc.cy - 46 - rise; + } + } + const [sx, sy] = worldToScreen(wx, wy); + ctx.save(); + ctx.globalAlpha = Math.min(1, Math.max(0, sp.t || 0) * 1.12); + if (sp.dmgTxt) { + const fpx = Math.max(11, 14 * zDraw); + ctx.font = `bold ${fpx}px ui-sans-serif, system-ui, sans-serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.strokeStyle = 'rgba(0, 0, 0, 0.62)'; + ctx.lineWidth = Math.max(2, 2.4 * zDraw); + ctx.strokeText(String(sp.dmgTxt), sx, sy); + ctx.fillStyle = '#f8fafc'; + ctx.fillText(String(sp.dmgTxt), sx, sy); + } else if (score10PopRec && score10PopRec.ready && score10PopRec.img && score10PopRec.img.naturalWidth > 0) { + const iw = score10PopRec.img.naturalWidth; + const ih = score10PopRec.img.naturalHeight; + const mh = Math.max(14, 20 * zDraw); + const sc = Math.min(1.35, mh / ih); + const dw = iw * sc; + const dh = ih * sc; + ctx.drawImage(score10PopRec.img, sx - dw / 2, sy - dh / 2, dw, dh); + } + ctx.restore(); + } + + const [bsx, bsy] = worldToScreen(bossC.cx, bossC.cy); + ctx.save(); + ctx.translate(bsx, bsy); + const limR = bossR * zDraw; + if (bossImgRec && bossImgRec.ready && bossImgRec.img && bossImgRec.img.naturalWidth > 0) { + const iw = bossImgRec.img.naturalWidth; + const ih = bossImgRec.img.naturalHeight; + const diam = limR * 2.1; + const scale = Math.min(diam / iw, diam / ih); + const dw = iw * scale; + const dh = ih * scale; + ctx.save(); + ctx.beginPath(); + ctx.arc(0, 0, limR * 1.05, 0, Math.PI * 2); + ctx.clip(); + ctx.drawImage(bossImgRec.img, -dw / 2, -dh / 2, dw, dh); + ctx.restore(); + } else { + ctx.beginPath(); + for (let k = 0; k < 6; k++) { + const ang = (k / 6) * Math.PI * 2 - Math.PI / 2; + const px = Math.cos(ang) * bossR * zDraw * 1.05; + const py = Math.sin(ang) * bossR * zDraw * 1.05; + if (k === 0) ctx.moveTo(px, py); + else ctx.lineTo(px, py); + } + ctx.closePath(); + ctx.fillStyle = 'rgba(0, 40, 60, 0.35)'; + ctx.fill(); + + const skullR = bossR * zDraw * 0.62; + const grd = ctx.createRadialGradient(-skullR * 0.2, -skullR * 0.25, skullR * 0.1, 0, 0, skullR); + grd.addColorStop(0, 'rgba(55, 50, 62, 0.98)'); + grd.addColorStop(1, 'rgba(22, 18, 30, 1)'); + ctx.fillStyle = grd; + ctx.beginPath(); + ctx.arc(0, 0, skullR, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = 'rgba(255, 40, 60, 0.95)'; + ctx.shadowColor = 'rgba(255, 0, 0, 0.8)'; + ctx.shadowBlur = 12 * zDraw; + ctx.beginPath(); + ctx.arc(-skullR * 0.32, -skullR * 0.12, skullR * 0.14, 0, Math.PI * 2); + ctx.arc(skullR * 0.32, -skullR * 0.12, skullR * 0.14, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowBlur = 0; + } + + /** HUD บอส: แถบชิดเหนือหัวบอส; โลโก้ Artboard10 + [hp/max] เหนือแถบ — bbHudS ลดสเกลทั้งแถว (ขอให้เล็กลง ~ครึ่ง) */ + const bbHudS = 0.5; + const fontPx = Math.max(8, 12.5 * zDraw * bbHudS); + ctx.font = `bold ${fontPx}px ui-monospace, monospace`; + ctx.textBaseline = 'middle'; + const hpStr = '[ ' + hpLeft + ' / ' + maxHp + ' ]'; + const hpW = ctx.measureText(hpStr).width; + const gapHp = 5 * zDraw * bbHudS; + + const powBgUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/boss-power-bg.png'); + const powFillUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/boss-power.png'); + const powBgRec = powBgUrl ? ensureGauntletAssetImage(powBgUrl) : null; + const powFillRec = powFillUrl ? ensureGauntletAssetImage(powFillUrl) : null; + + /** ความสูงหลอด: ยึดความกว้างก่อน — คูณ bbHudS ให้สัดส่วนเทียบหัวบอสเล็กลง */ + const barHMin = Math.max(9, 10.5 * zDraw) * bbHudS; + const barHMax = Math.max(17, 21 * zDraw) * bbHudS; + + const titleImgUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/Artboard%2010.png'); + const titleImgRec = titleImgUrl ? ensureGauntletAssetImage(titleImgUrl) : null; + let imgW = 0; + let imgH = 0; + if (titleImgRec && titleImgRec.ready && titleImgRec.img && titleImgRec.img.naturalWidth > 0) { + const iw = titleImgRec.img.naturalWidth; + const ih = titleImgRec.img.naturalHeight; + let barHForTitle = barHMin; + if (powBgRec && powBgRec.ready && powBgRec.img && powBgRec.img.naturalWidth > 0) { + const biw = powBgRec.img.naturalWidth; + const bijh = powBgRec.img.naturalHeight; + const wEst = Math.max(limR * 2.12 * bbHudS, hpW + gapHp + iw * 0.35 * bbHudS); + barHForTitle = Math.min(barHMax, Math.max(barHMin, bijh * (wEst / biw))); + } + const capTitle = Math.min(17 * zDraw * bbHudS, Math.max(fontPx * 1.15, barHForTitle * 0.88)); + const maxH = Math.max(fontPx * 1.12, 14 * zDraw * bbHudS, capTitle); + const sc = Math.min(1.0, maxH / ih); + imgW = iw * sc; + imgH = ih * sc; + } + + let rowW = (imgW > 0 ? imgW + gapHp : 0) + hpW; + if (imgW <= 0) { + const legacyPre = 'MEGA VIRUS : '; + rowW = ctx.measureText(legacyPre).width + hpW; + } + const barPadRow = 8 * zDraw * bbHudS; + const barWTarget = Math.max(limR * 2.08 * bbHudS, rowW + barPadRow); + + let barDw = barWTarget; + let barDh = barHMin; + if (powBgRec && powBgRec.ready && powBgRec.img && powBgRec.img.naturalWidth > 0) { + const iw = powBgRec.img.naturalWidth; + const ih = powBgRec.img.naturalHeight; + const naturalH = ih * (barWTarget / iw); + barDh = Math.min(barHMax, Math.max(barHMin, naturalH)); + } + + const gapBarBoss = 4 * zDraw * bbHudS; + const gapTitleBar = 5 * zDraw * bbHudS; + const barBottomEdge = -limR * 1.0 - gapBarBoss; + const barTopY = barBottomEdge - barDh; + const bx = -barDw / 2; + const by = barTopY; + const ratHp = Math.max(0, Math.min(1, hpLeft / maxHp)); + + const titleRowH = Math.max(imgH, fontPx * 1.12); + const labelY = barTopY - gapTitleBar - titleRowH * 0.5; + + let xLeft = -(imgW + (imgW > 0 ? gapHp : 0) + hpW) / 2; + if (imgW > 0) { + ctx.drawImage(titleImgRec.img, xLeft, labelY - imgH / 2, imgW, imgH); + xLeft += imgW + gapHp; + } else { + ctx.textAlign = 'left'; + ctx.fillStyle = 'rgba(255, 100, 200, 0.9)'; + const legacyPre = 'MEGA VIRUS : '; + const pw = ctx.measureText(legacyPre).width; + xLeft = -(pw + hpW) / 2; + ctx.fillText(legacyPre, xLeft, labelY); + xLeft += pw; + } + ctx.textAlign = 'left'; + ctx.fillStyle = '#ffffff'; + ctx.strokeStyle = 'rgba(0, 0, 0, 0.22)'; + ctx.lineWidth = Math.max(0.5, 1 * zDraw * bbHudS); + ctx.strokeText(hpStr, xLeft, labelY); + ctx.fillText(hpStr, xLeft, labelY); + + if (powBgRec && powBgRec.ready && powBgRec.img && powBgRec.img.naturalWidth > 0) { + ctx.drawImage(powBgRec.img, bx, by, barDw, barDh); + if (ratHp > 0 && powFillRec && powFillRec.ready && powFillRec.img && powFillRec.img.naturalWidth > 0) { + const fiw = powFillRec.img.naturalWidth; + const fih = powFillRec.img.naturalHeight; + const padIn = barDw * 0.04; + const innerW = barDw - padIn * 2; + const innerH = barDh - padIn * 1.2; + const fsc2 = Math.min(innerW / fiw, innerH / fih); + const fdw2 = fiw * fsc2; + const fdh2 = fih * fsc2; + const fx0 = bx + (barDw - fdw2) * 0.5; + const fy0 = by + (barDh - fdh2) * 0.5; + const srcW = fiw * ratHp; + ctx.drawImage(powFillRec.img, 0, 0, srcW, fih, fx0, fy0, fdw2 * ratHp, fdh2); + } + } else { + ctx.fillStyle = 'rgba(0,0,0,0.55)'; + ctx.fillRect(bx, by, barDw, barDh); + ctx.fillStyle = 'rgba(255, 80, 200, 0.85)'; + ctx.fillRect(bx, by, barDw * ratHp, barDh); + ctx.strokeStyle = 'rgba(180, 255, 255, 0.5)'; + ctx.strokeRect(bx, by, barDw, barDh); + } + ctx.restore(); + + /** จุดก้นลูกโป่งหลังหมุน tilt (รัศมีจากกลางสู่ก้นใน local) */ + function balloonBossBalloonKnotScreen(bx, by, dw, dh, tiltRad) { + const lx = 0; + const ly = dh * 0.5; + const c = Math.cos(tiltRad); + const s = Math.sin(tiltRad); + return { x: bx + lx * c - ly * s, y: by + lx * s + ly * c }; + } + + /** ลูกโป่งด้านบน +50% — กระจุกเอียงซ้าย/กลางตรง/ขวา (tilt) + เชือกบางไปจุดผูบนวง · Bouquet tilt like mock */ + function drawBalloonsCluster(sx, sy, count, col, z, slot1to6, playerRingR) { + const slotIdx = Math.max(0, Math.min(5, (Math.floor(Number(slot1to6)) || 1) - 1)); + const perSeat = String((playBalloonBossPlayerBalloonImageUrls[slotIdx] || '')).trim(); + const fallback = String(playBalloonBossPlayerBalloonFallbackUrl || '').trim(); + const bRec = resolveBalloonBossBalloonSpriteRec(perSeat, fallback); + const n = Math.max(0, Math.min(8, count | 0)); + const ringRpx = playerRingR != null && playerRingR > 0 ? playerRingR : 22 * z; + const sz = 1.5; + const tw = 22 * sz * z; + const th = 27 * sz * z; + const stackAnchor = ringRpx + 10 * z; + const tieX = sx; + const tieY = sy - ringRpx * 0.98; + const tiltStep = 0.5; + const spreadX = 5.4 * z; + const baseY = sy - stackAnchor - 1.5 * z; + const items = []; + for (let b = 0; b < n; b++) { + const rel = b - (n - 1) / 2; + const tilt = rel * tiltStep; + const bx = sx + rel * spreadX; + const by = baseY + Math.abs(rel) * 2.1 * z; + const bImg = bRec && bRec.img; + let dw = 0; + let dh = 0; + if (bImg && bImg.complete && bImg.naturalWidth > 0) { + const iw = bImg.naturalWidth; + const ih = bImg.naturalHeight; + const sc = Math.min(tw / iw, th / ih); + dw = iw * sc; + dh = ih * sc; + } else { + const ry = 12 * sz * z; + dw = 10 * sz * z * 2; + dh = ry * 2; + } + const knot = balloonBossBalloonKnotScreen(bx, by, dw, dh, tilt); + items.push({ bx, by, dw, dh, bImg, tilt, knotX: knot.x, knotY: knot.y }); + } + if (n > 0) { + ctx.strokeStyle = 'rgba(55, 48, 42, 0.55)'; + ctx.lineWidth = Math.max(0.65, 0.78 * z); + ctx.lineCap = 'butt'; + ctx.lineJoin = 'bevel'; + for (let i = 0; i < items.length; i++) { + const it = items[i]; + ctx.beginPath(); + ctx.moveTo(tieX, tieY); + ctx.lineTo(it.knotX, it.knotY); + ctx.stroke(); + } + } + for (let i = 0; i < items.length; i++) { + const it = items[i]; + const bImg = it.bImg; + if (bImg && bImg.complete && bImg.naturalWidth > 0) { + const iw = bImg.naturalWidth; + const ih = bImg.naturalHeight; + const sc = Math.min(tw / iw, th / ih); + const dw = iw * sc; + const dh = ih * sc; + ctx.save(); + ctx.translate(it.bx, it.by); + ctx.rotate(it.tilt); + ctx.drawImage(bImg, -dw / 2, -dh / 2, dw, dh); + ctx.restore(); + } else { + ctx.save(); + ctx.translate(it.bx, it.by); + ctx.rotate(it.tilt); + ctx.fillStyle = col; + ctx.beginPath(); + ctx.ellipse(0, 0, 10 * sz * z, 12 * sz * z, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(255,255,255,0.35)'; + ctx.lineWidth = 1; + ctx.stroke(); + ctx.restore(); + } + } + } + + refs.forEach((entry, idx) => { + const ent = entry.ref; + if (ent.balloonBossCx == null || ent.balloonBossCy == null) return; + const [sx, sy] = worldToScreen(ent.balloonBossCx, ent.balloonBossCy); + const col = BALLOON_BOSS_PLAYER_COLORS[idx % BALLOON_BOSS_PLAYER_COLORS.length]; + const z = zDraw; + const skinSlot = (typeof ent.balloonBossSkinSlot === 'number' && ent.balloonBossSkinSlot >= 1 && ent.balloonBossSkinSlot <= 6) + ? ent.balloonBossSkinSlot + : ((idx % 6) + 1); + const ringR = 25 * z; + /** จุดกลางฟองบนจอ — ยกจากจุด world เล็กน้อยให้ตัวอยู่กลางวง ลูกโป่งชิดขอบบน (ตาม mock) */ + const bubbleCy = sy - ringR * 0.42; + /** มุมลูกศร Artboard 9 — หมุนอัตโนมัติ; Mega Virus ยิงตามมุมนี้ ณ ตอนกด Space */ + const angRing = (typeof ent.balloonBossAimRad === 'number' && Number.isFinite(ent.balloonBossAimRad)) + ? ent.balloonBossAimRad + : ((timeMs * 0.0019 + idx * 1.73) % (Math.PI * 2)); + /** หันหน้าเข้ากล้อง (mock) — ไม่ใช้ทิศออกจากบอสในเลเยอร์นี้ */ + const faceDir = 'down'; + const slotIdxRing = Math.max(0, Math.min(5, skinSlot - 1)); + const perRing = String((playBalloonBossPlayerBalloonImageUrls[slotIdxRing] || '')).trim(); + const fbRing = String(playBalloonBossPlayerBalloonFallbackUrl || '').trim(); + /** กรอบฟองรอบตัว = fallback (Artboard 9) เท่านั้น — ลูกโป่งต่อที่นั่งใช้แค่กองด้านบน · Ring frame is never per-seat balloon art */ + const ringRec = fbRing + ? resolveBalloonBossBalloonSpriteRec('', fbRing) + : resolveBalloonBossBalloonSpriteRec(perRing, ''); + ctx.save(); + if (ent.balloonBossEliminated) ctx.globalAlpha = 0.38; + const ringImg = ringRec && ringRec.img; + const ringPainted = !!(ringImg && ringImg.complete && ringImg.naturalWidth > 0); + if (ringPainted) { + const iw = ringImg.naturalWidth; + const ih = ringImg.naturalHeight; + const maxBox = 2 * ringR * 1.14; + const sc = Math.min(maxBox / iw, maxBox / ih); + const dw = iw * sc; + const dh = ih * sc; + /** หางฟอง (Artboard +Y) หมุนตาม angRing — tail ชี้ทิศยิง */ + ctx.save(); + ctx.translate(sx, bubbleCy); + ctx.rotate(angRing - Math.PI / 2); + ctx.drawImage(ringImg, -dw / 2, -dh / 2, dw, dh); + ctx.restore(); + } + if (!ringPainted) { + ctx.strokeStyle = 'rgba(255, 255, 255, 0.72)'; + ctx.lineWidth = Math.max(1.2, 1.6 * z); + ctx.beginPath(); + ctx.arc(sx, bubbleCy, ringR, 0, Math.PI * 2); + ctx.stroke(); + } else { + ctx.strokeStyle = 'rgba(255, 255, 255, 0.22)'; + ctx.lineWidth = Math.max(1, 1.1 * z); + ctx.beginPath(); + ctx.arc(sx, bubbleCy, ringR, 0, Math.PI * 2); + ctx.stroke(); + } + drawBalloonsCluster(sx, bubbleCy, ent.balloonBossBalloons | 0, col, z, skinSlot, ringR); + const nm = String(ent.nickname || '').slice(0, 12); + ctx.font = `${Math.max(9, 10 * z)}px system-ui, "Kanit", sans-serif`; + ctx.textAlign = 'center'; + ctx.fillStyle = 'rgba(248, 250, 252, 0.95)'; + ctx.strokeStyle = 'rgba(0, 10, 30, 0.85)'; + ctx.lineWidth = 3; + let label = nm; + if (entry.id === leaderId && leaderId != null) label = '♔ ' + nm; + const labelY = bubbleCy + ringR + 8 * z; + ctx.strokeText(label, sx, labelY); + ctx.fillText(label, sx, labelY); + const charImg = getPlayTintedAvatarSource( + getAvatarImg(ent.characterId, faceDir, timeMs, !!ent.isWalking), + ent.characterId, faceDir, timeMs, !!ent.isWalking, + ent.playTint || playTintFromPeerId(entry.id) + ); + const size = 26 * z; + if (charImg && ((charImg.tagName === 'CANVAS' && charImg.width > 0) || (charImg.complete && charImg.naturalWidth))) { + const iw = charImg.tagName === 'CANVAS' ? charImg.width : charImg.naturalWidth; + const ih = charImg.tagName === 'CANVAS' ? charImg.height : charImg.naturalHeight; + const sc = Math.min(size / iw, size / ih, 1); + const dw = iw * sc, dh = ih * sc; + const charTop = bubbleCy - dh * 0.48; + ctx.drawImage(charImg, 0, 0, iw, ih, sx - dw / 2, charTop, dw, dh); + } else { + ctx.fillStyle = col; + ctx.beginPath(); + ctx.arc(sx, bubbleCy, 9 * z, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(0, 10, 30, 0.45)'; + ctx.lineWidth = Math.max(1, 1.2 * z); + ctx.stroke(); + } + ctx.restore(); + }); + + const bulletCodeUrl = normalizeGauntletAssetUrlForPlay('/Game/img/MegaVirus/bullet-code.png'); + const bulletCodeRec = bulletCodeUrl ? ensureGauntletAssetImage(bulletCodeUrl) : null; + for (let i = 0; i < balloonBossPlayerBullets.length; i++) { + const b = balloonBossPlayerBullets[i]; + const [bx, by] = worldToScreen(b.x, b.y); + ctx.save(); + ctx.translate(bx, by); + /** bullet-code.png หัวลูกศรชี้ขวา (+x) — หมุนตามทิศความเร็วโดยไม่บวก π/2 (ก่อนหน้านี้ดูเหมือนยิงเฉียง) */ + ctx.rotate(Math.atan2(b.vy, b.vx)); + if (bulletCodeRec && bulletCodeRec.ready && bulletCodeRec.img && bulletCodeRec.img.naturalWidth > 0) { + const iw = bulletCodeRec.img.naturalWidth; + const ih = bulletCodeRec.img.naturalHeight; + const mh = Math.max(10, 15 * zDraw); + const sc = Math.min(1.4, mh / ih); + const dw = iw * sc; + const dh = ih * sc; + ctx.drawImage(bulletCodeRec.img, -dw / 2, -dh / 2, dw, dh); + } else { + ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; + ctx.beginPath(); + ctx.moveTo(0, -8 * zDraw); + ctx.lineTo(-5 * zDraw, 6 * zDraw); + ctx.lineTo(5 * zDraw, 6 * zDraw); + ctx.closePath(); + ctx.fill(); + ctx.fillStyle = 'rgba(0, 255, 200, 0.5)'; + ctx.font = `${Math.max(7, 8 * zDraw)}px monospace`; + ctx.textAlign = 'center'; + ctx.fillText('01001', 0, 12 * zDraw); + } + ctx.restore(); + } + for (let i = 0; i < balloonBossBossBullets.length; i++) { + const b = balloonBossBossBullets[i]; + const [bx, by] = worldToScreen(b.x, b.y); + ctx.fillStyle = 'rgba(255, 60, 120, 0.92)'; + ctx.beginPath(); + ctx.arc(bx, by, Math.max(3, 4.5 * zDraw), 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = 'rgba(255, 200, 255, 0.6)'; + ctx.stroke(); + } + } + + function jumpSurviveCollectPlatformCells(md) { + const w = md.width || 20, h = md.height || 15; + const pa = md.jumpSurvivePlatformArea; + const out = []; + if (!pa) return out; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (pa[y] && pa[y][x] === 1) out.push({ x, y }); + } + } + return out; + } + + /** ความสูงแมปเป็นคาบวนของแพลตฟอร์ม — แถวที่เลื่อนออกล่างจะวนมาโผล่บน */ + function jumpSurvivePlatformPeriodPx(md) { + return (md.height || 15) * tileSize; + } + + /** + * ยืนบนหน้าแพลตฟอร์มช่อง (tx,ty) — เลือกชั้น k ที่ใกล้ hintY (กล้อง / กลางแมป) + */ + function jumpSurviveGridPosStandingOnPlatform(md, tx, ty, scrollPx) { + const ts = tileSize; + const h = md.height || 15; + const period = jumpSurvivePlatformPeriodPx(md); + const { cw, ch } = getCharacterFootprintWH(md); + const rawTop = ty * ts + scrollPx; + const hintY = (jumpSurviveCamCenterY > 1) + ? jumpSurviveCamCenterY + : h * ts * 0.72; + const k = Math.round((hintY - rawTop - ts * 0.35) / period); + const platTop = rawTop + k * period; + const feetY = platTop; + const pxc = (tx + 0.5) * ts; + return { + x: pxc / ts - cw * 0.5, + y: feetY / ts - ch, + }; + } + + function jumpSurviveGridPosFromTileCenter(md, tx, ty) { + return jumpSurviveGridPosStandingOnPlatform(md, tx, ty, 0); + } + + function jumpSurviveRandomStandGridPos(md) { + const cells = jumpSurviveCollectPlatformCells(md); + const sc = jumpSurvivePlatformScrollPx; + if (cells.length) { + const c = cells[Math.floor(Math.random() * cells.length)]; + return jumpSurviveGridPosStandingOnPlatform(md, c.x, c.y, sc); + } + const sp = md.spawn || { x: 1, y: 1 }; + return jumpSurviveGridPosStandingOnPlatform(md, Number(sp.x) || 1, Number(sp.y) || 1, sc); + } + + function jumpSurviveTileHasPlatformCellPlay(md, tx, ty) { + const pa = md && md.jumpSurvivePlatformArea; + const h = md.height || (pa && pa.length) || 15; + const w = md.width || 20; + if (!pa || ty < 0 || ty >= h) return false; + const row = pa[ty]; + if (!row || tx < 0 || tx >= w) return false; + return row[tx] === 1; + } + + /** หาแพลตฟอร์มในคอลัมน์ tx ใกล้ ty — กันจุดเกิดช่องว่างแล้วลอย */ + function jumpSurviveFindPlatformColumnPlay(md, tx, tyHint) { + const w = md.width || 20, h = md.height || 15; + const tcx = Math.max(0, Math.min(w - 1, Math.floor(tx))); + let ty = Math.max(0, Math.min(h - 1, Math.floor(tyHint))); + if (jumpSurviveTileHasPlatformCellPlay(md, tcx, ty)) return { x: tcx, y: ty }; + for (let d = 1; d < h; d++) { + const yb = ty + d; + if (yb < h && jumpSurviveTileHasPlatformCellPlay(md, tcx, yb)) return { x: tcx, y: yb }; + const ya = ty - d; + if (ya >= 0 && jumpSurviveTileHasPlatformCellPlay(md, tcx, ya)) return { x: tcx, y: ya }; + } + return { x: tcx, y: ty }; + } + + /** จุดเกิดตามลำดับ join / P1–P6 → world ยืนบนแพลตฟอร์ม (ไม่ทับสุ่ม pool) */ + function jumpSurviveSpawnWorldFromJoinOrderPlay(md, joinOrder) { + const sp = pickSpawnForJoinPlay(md, joinOrder | 0); + let tx = Math.floor(Number(sp.x)); + let ty = Math.floor(Number(sp.y)); + if (!Number.isFinite(tx) || !Number.isFinite(ty)) { + const fb = md.spawn || { x: 1, y: 1 }; + tx = Math.floor(Number(fb.x)) || 1; + ty = Math.floor(Number(fb.y)) || 1; + } + const cell = jumpSurviveFindPlatformColumnPlay(md, tx, ty); + const sc = jumpSurvivePlatformScrollPx; + const p = jumpSurviveGridPosStandingOnPlatform(md, cell.x, cell.y, sc); + return { + x: p.x + (Math.random() - 0.5) * 0.05, + y: p.y + (Math.random() - 0.5) * 0.05, + }; + } + + /** ทดสอบจากเอดิเตอร์: วางคน+บอทบนแพลตฟอร์ม (ไม่ให้เกิดบน spawnArea บนฟ้าแล้วลอย) */ + function applyJumpSurvivePreviewSpawnLayout(onlyBots) { + if (!mapData || mapData.gameType !== 'jump_survive') return; + + function stampJumpBotState(o) { + if (!o) return; + o.jumpSurviveVy = 0; + o.jumpSurviveOnGround = true; + o.jumpSurviveEliminated = false; + o.tx = o.x; + o.ty = o.y; + } + + if (isJumpSurviveMissionUiMapPlay()) { + function snapEnt(ent) { + if (!ent) return; + const ordRaw = Number(ent.spawnJoinOrder); + const jo = Number.isFinite(ordRaw) ? Math.max(0, Math.floor(ordRaw)) : 0; + const pos = jumpSurviveSpawnWorldFromJoinOrderPlay(mapData, jo); + ent.x = pos.x; + ent.y = pos.y; + stampJumpBotState(ent); + } + if (!onlyBots) { + snapEnt(me); + const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + realIds.forEach((rid) => snapEnt(others.get(rid))); + } + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + botIds.forEach((bid) => snapEnt(others.get(bid))); + jumpSurviveInitRuntime(); + return; + } + + const cells = jumpSurviveCollectPlatformCells(mapData); + cells.sort((a, b) => b.y - a.y || a.x - b.x); + const sc = jumpSurvivePlatformScrollPx; + const pool = cells.length + ? cells.map((c) => jumpSurviveGridPosStandingOnPlatform(mapData, c.x, c.y, sc)) + : [jumpSurviveGridPosStandingOnPlatform(mapData, Number(mapData.spawn && mapData.spawn.x) || 1, Number(mapData.spawn && mapData.spawn.y) || 1, sc)]; + const realIds = [...others.keys()].filter((id) => !isPreviewBotId(id)).sort(); + const botIds = [...others.keys()].filter(isPreviewBotId).sort(); + + if (onlyBots) { + let idx = 1 + realIds.length; + botIds.forEach((bid) => { + const o = others.get(bid); + if (!o || !pool.length) return; + const p = pool[idx % pool.length]; + idx++; + o.x = p.x + (Math.random() - 0.5) * 0.08; + o.y = p.y + (Math.random() - 0.5) * 0.08; + stampJumpBotState(o); + }); + return; + } + + let idx = 0; + function assignEnt(ent) { + if (!ent || !pool.length) return; + const p = pool[idx % pool.length]; + idx++; + ent.x = p.x + (Math.random() - 0.5) * 0.08; + ent.y = p.y + (Math.random() - 0.5) * 0.08; + stampJumpBotState(ent); + } + assignEnt(me); + realIds.forEach((rid) => assignEnt(others.get(rid))); + botIds.forEach((bid) => assignEnt(others.get(bid))); + jumpSurviveInitRuntime(); + } + + /** + * ฟิสิกส์กระโดดร่วม (ผู้เล่น + บอท preview) + * ตายจาก: hazard ที่วาง, บีบระหว่าง solid+object wall, หรือตกออกนอกขอบโลกแมป (ไม่ใช้ขอบกล้อง) + * @returns {{ pxc: number, feetY: number, vy: number, onGround: boolean, died: boolean }} + */ + function jumpSurvivePhysicsIntegrate(md, opts) { + const { + dt, + cw, ch, + pxc0, feetY0, vy0, wasOnGround, + jumpQueued, + moveLeft, moveRight, + } = opts; + const w = md.width || 20, h = md.height || 15; + const ts = tileSize; + const bw = ts * 0.34; + const bh = ts * 0.9; + let pxc = pxc0; + let feetY = feetY0; + let vy = vy0; + let onGround = wasOnGround; + + const gFull = Number(md.jumpSurviveGravity) > 0 ? Number(md.jumpSurviveGravity) : 3200; + const g = gFull * dt; + let jumpV0; + if (Number(md.jumpSurviveJumpImpulse) > 0) { + jumpV0 = -Number(md.jumpSurviveJumpImpulse); + } else { + const mult = (typeof playJumpSurviveJumpHeightMult === 'number' && playJumpSurviveJumpHeightMult > 0) + ? playJumpSurviveJumpHeightMult + : 1.5; + const charHpixels = ch * ts; + const impulse = Math.sqrt(Math.max(1e-6, 2 * gFull * mult * charHpixels)); + jumpV0 = -Math.min(impulse, ts * 36); + } + let moveX = (Number(md.jumpSurviveMovePxPerSec) > 0 ? Number(md.jumpSurviveMovePxPerSec) : 200) * dt; + if (isJumpSurviveMissionUiMapPlay()) moveX *= 1.35; + + if (jumpQueued && onGround) { + vy = jumpV0; + onGround = false; + } + vy += g; + + let nx = pxc; + if (moveLeft) nx -= moveX; + if (moveRight) nx += moveX; + nx = Math.max(bw + 1, Math.min(w * ts - bw - 1, nx)); + + const headY0 = feetY - bh; + const tryL = nx - bw; + const tryR = nx + bw; + if (!jumpSurviveOverlapsSolidForHorzMove(md, tryL, headY0, tryR, feetY)) { + pxc = nx; + } + + feetY += vy * dt; + let headTop = feetY - bh; + if (jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY)) { + if (vy > 0) { + const step = ts * 0.035; + const penetrationEst = Math.abs(vy) * dt + ts * 0.55; + const maxSteps = Math.min(160, Math.max(28, Math.ceil(penetrationEst / step) + 6)); + for (let s = 0; s < maxSteps; s++) { + feetY -= step; + headTop = feetY - bh; + if (!jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY)) { + vy = 0; + onGround = true; + break; + } + } + } else if (vy < 0) { + const stepUp = ts * 0.035; + const penetrationEstUp = Math.abs(vy) * dt + ts * 0.55; + const maxStepsUp = Math.min(160, Math.max(28, Math.ceil(penetrationEstUp / stepUp) + 6)); + for (let s = 0; s < maxStepsUp; s++) { + feetY += stepUp; + headTop = feetY - bh; + if (!jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY)) { + vy = 0; + break; + } + } + } + } else { + onGround = false; + } + + headTop = feetY - bh; + const headBandBottom = headTop + bh * 0.42; + if (jumpSurviveOverlapsSolid(md, pxc - bw, headTop, pxc + bw, feetY) && + jumpSurviveOverlapsObjectWall(md, pxc - bw, headTop, pxc + bw, headBandBottom)) { + return { pxc, feetY, vy, onGround, died: true }; + } + + headTop = feetY - bh; + if (jumpSurviveOverlapsHazardArea(md, pxc - bw, headTop, pxc + bw, feetY)) { + return { pxc, feetY, vy, onGround, died: true }; + } + + headTop = feetY - bh; + /* ไม่ใช้ขอบกล้อง (cam topKill/botKill) — ตายเฉพาะ hazard ที่วาง + ตกออกนอกโลกแมปจริง (ไม่ตายในอากาศระหว่างแพลตฟอร์ม) */ + const mapHpx = h * ts; + if (feetY > mapHpx + ts * 8 || headTop < -ts * 32) { + return { pxc, feetY, vy, onGround, died: true }; + } + + return { pxc, feetY, vy, onGround, died: false }; + } + + function jumpSurviveEntityFromHitbox(o, md, pxc, feetYpx) { + const { cw, ch } = getCharacterFootprintWH(md); + o.x = pxc / tileSize - cw * 0.5; + o.y = feetYpx / tileSize - ch; + } + + function jumpSurviveInitRuntime() { + jumpSurviveLastTickMs = performance.now(); + jumpSurviveSessionStartMs = performance.now(); + jumpSurviveVy = 0; + jumpSurviveJumpQueued = false; + jumpSurviveOnGround = false; + jumpSurvivePlatformScrollPx = 0; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ts = tileSize; + if (isJumpSurviveMissionUiMapPlay()) { + const w = mapData.width || 20, h = mapData.height || 15; + jumpSurviveCamCenterX = (w * ts) * 0.5; + jumpSurviveCamCenterY = (h * ts) * 0.5; + } else { + jumpSurviveCamCenterX = (me.x + cw * 0.5) * ts; + jumpSurviveCamCenterY = (me.y + ch * 0.5) * ts; + } + } + + function jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const x0 = Math.floor(left / ts); + const x1 = Math.floor((right - 1e-6) / ts); + const y0 = Math.floor(top / ts); + const y1 = Math.floor((bottom - 1e-6) / ts); + for (let ty = y0; ty <= y1; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (tx < 0 || tx >= w) return true; + if (ty < 0) continue; + /* แพลตฟอร์มวนแนวตั้ง (period = h*ts) — worldY เกิน h*ts เป็นปกติ ห้ามถือ ty>=h เป็น “ซีลพื้นแมป” ไม่งั้น solid+กำแพงข้าง = ตายคาแอร์ */ + if (ty >= h) continue; + const ob = md.objects && md.objects[ty] && md.objects[ty][tx]; + if (ob === 1) return true; + } + } + return false; + } + + function jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, scrollPx) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const pa = md.jumpSurvivePlatformArea; + if (!pa) return false; + const period = jumpSurvivePlatformPeriodPx(md); + if (period < ts) return false; + const x0 = Math.max(0, Math.floor(left / ts)); + const x1 = Math.min(w - 1, Math.floor((right - 1e-6) / ts)); + const vm = ts * 2; + for (let ty = 0; ty < h; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (!pa[ty] || pa[ty][tx] !== 1) continue; + const rawTop = ty * ts + scrollPx; + let kMin = Math.ceil((top - rawTop - ts - vm) / period); + let kMax = Math.floor((bottom - rawTop + vm) / period); + /* ceil/floor กับช่วงสั้น ๆ อาจได้ kMin > kMax → ไม่ลอง k เลย แล้วตกทะลุแพลตฟอร์ม */ + if (kMin > kMax) { + const kGuess = Math.round((bottom - rawTop - ts * 0.5) / period); + kMin = kGuess; + kMax = kGuess; + } + for (let k = kMin; k <= kMax; k++) { + const platTop = rawTop + k * period; + const platBot = platTop + ts; + const tileLeft = tx * ts; + const tileRight = (tx + 1) * ts; + if (platBot > top && platTop < bottom && right > tileLeft && left < tileRight) return true; + } + } + } + return false; + } + + /** แถวแพลตฟอร์มบนสุด/ล่างสุดของแมป (มีช่องอย่างน้อยหนึ่งช่อง) — ใช้เป็นเพดาน/พื้นตาย */ + function jumpSurvivePlatformEdgeKillRows(md) { + const pa = md && md.jumpSurvivePlatformArea; + if (!pa) return null; + const h = Math.min(pa.length, md.height || pa.length || 15); + const wM = md.width || 20; + let minTy = h; + let maxTy = -1; + for (let ty = 0; ty < h; ty++) { + const row = pa[ty]; + if (!row) continue; + const wlim = Math.min(row.length || 0, wM); + let rowHas = false; + for (let tx = 0; tx < wlim; tx++) { + if (row[tx] === 1) { + rowHas = true; + break; + } + } + if (!rowHas) continue; + if (ty < minTy) minTy = ty; + if (ty > maxTy) maxTy = ty; + } + if (minTy > maxTy) return null; + if (minTy === maxTy) return null; + return { minTy, maxTy }; + } + + /** ชนแพลตฟอร์มแถวเพดานเท่านั้น (รวมเลื่อนแบบคาบ) = ตายทันที — ไม่ฆ่าที่แถวพื้นล่าง */ + function jumpSurviveOverlapsEdgeKillPlatforms(md, left, top, right, bottom, scrollPx) { + const edges = jumpSurvivePlatformEdgeKillRows(md); + if (!edges) return false; + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const pa = md.jumpSurvivePlatformArea; + if (!pa) return false; + const period = jumpSurvivePlatformPeriodPx(md); + if (period < ts) return false; + const x0 = Math.max(0, Math.floor(left / ts)); + const x1 = Math.min(w - 1, Math.floor((right - 1e-6) / ts)); + const vm = ts * 2; + const overlapsRowTy = function (ty) { + if (ty < 0 || ty >= h || !pa[ty]) return false; + for (let tx = x0; tx <= x1; tx++) { + if (pa[ty][tx] !== 1) continue; + const rawTop = ty * ts + scrollPx; + let kMin = Math.ceil((top - rawTop - ts - vm) / period); + let kMax = Math.floor((bottom - rawTop + vm) / period); + if (kMin > kMax) { + const kGuess = Math.round((bottom - rawTop - ts * 0.5) / period); + kMin = kGuess; + kMax = kGuess; + } + const zSlack = ts * 0.08; + for (let k = kMin; k <= kMax; k++) { + const platTop = rawTop + k * period; + const platBot = platTop + ts; + const tileLeft = tx * ts; + const tileRight = (tx + 1) * ts; + if (platBot > top - zSlack && platTop < bottom + zSlack && right > tileLeft && left < tileRight) return true; + } + } + return false; + }; + if (overlapsRowTy(edges.minTy)) return true; + return false; + } + + function jumpSurviveOverlapsSolid(md, left, top, right, bottom) { + if (jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom)) return true; + return jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, jumpSurvivePlatformScrollPx); + } + + /** + * ชนแนวนอน: objects เต็มกล่อง; แพลตฟอร์มไม่นับช่วงปลายเท้าเล็กน้อย + * (ถ้าใช้กล่องเต็มกับแพลตฟอร์ม ขณะยืนบนแพลตฟอร์มมักทับช่องแนวตั้ง — เดินซ้ายขวาถูกปฏิเสธทั้งก้อน) + */ + function jumpSurviveOverlapsSolidForHorzMove(md, left, top, right, bottom) { + if (jumpSurviveOverlapsObjectSolids(md, left, top, right, bottom)) return true; + const ts = tileSize; + const footSlop = ts * 0.16; + const platformBottom = bottom - footSlop; + if (platformBottom <= top + ts * 0.05) return false; + return jumpSurviveOverlapsPlatforms(md, left, top, right, platformBottom, jumpSurvivePlatformScrollPx); + } + + /** Head/body overlaps กำแพง (objects=1) เท่านั้น — ใช้ตรวจชนฝาบนแล้วตาย */ + function jumpSurviveOverlapsObjectWall(md, left, top, right, bottom) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + const x0 = Math.floor(left / ts); + const x1 = Math.floor((right - 1e-6) / ts); + const y0 = Math.floor(top / ts); + const y1 = Math.floor((bottom - 1e-6) / ts); + for (let ty = y0; ty <= y1; ty++) { + for (let tx = x0; tx <= x1; tx++) { + if (tx < 0 || tx >= w || ty < 0 || ty >= h) continue; + const ob = md.objects && md.objects[ty] && md.objects[ty][tx]; + if (ob === 1) return true; + } + } + return false; + } + + function jumpSurviveSyncMeFromHitbox(pxc, feetYpx) { + jumpSurviveEntityFromHitbox(me, mapData, pxc, feetYpx); + } + + function jumpSurviveCameraTargetPx(md) { + const ts = tileSize; + const w = md.width || 20, h = md.height || 15; + if (isJumpSurviveMissionUiMapPlay()) { + return { px: (w * ts) * 0.5, py: (h * ts) * 0.5 }; + } + const { cw, ch } = getCharacterFootprintWH(md); + const cx = (e) => (e.x + cw * 0.5) * ts; + const cy = (e) => (e.y + ch * 0.5) * ts; + if (!jumpSurviveEliminated) return { px: cx(me), py: cy(me) }; + const aliveHumanIds = [...others.keys()].filter((id) => { + if (isPreviewBotId(id)) return false; + const o = others.get(id); + return o && !o.jumpSurviveEliminated; + }); + const aliveBotIds = [...others.keys()].filter((id) => { + if (!isPreviewBotId(id)) return false; + const o = others.get(id); + return o && !o.jumpSurviveEliminated; + }); + const useIds = aliveHumanIds.length ? aliveHumanIds : aliveBotIds; + if (!useIds.length) return { px: cx(me), py: cy(me) }; + let sx = 0, sy = 0; + useIds.forEach((id) => { + const o = others.get(id); + if (!o) return; + sx += cx(o); + sy += cy(o); + }); + const n = useIds.length; + return { px: sx / n, py: sy / n }; + } + + /** มีแพลตฟอร์มใต้จุดเล็กน้อยข้างหน้าแนว wishDir หรือไม่ — กันบอทเดินหลุมแล้วตกจอ */ + function jumpSurviveBotHasFloorAhead(md, pxc, feetY, wishDir, scrollPx) { + if (!wishDir) return true; + const ts = tileSize; + const bw = ts * 0.34; + const probeX = pxc + wishDir * (bw + ts * 0.82); + const pad = ts * 0.24; + const left = probeX - pad; + const right = probeX + pad; + const top = feetY - ts * 0.45; + const bottom = feetY + ts * 8; + return jumpSurviveOverlapsPlatforms(md, left, top, right, bottom, scrollPx); + } + + function stepJumpSurvivePreviewBots(dt, halfViewH) { + if (!playBotsEnabled() || !mapData || !isJumpSurvive()) return; + const md = mapData; + const { cw, ch } = getCharacterFootprintWH(md); + const ts = tileSize; + const wpx = (md.width || 20) * ts; + const centerPx = wpx * 0.5; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + if (o.jumpSurviveEliminated) return; + if (typeof o.jumpSurviveVy !== 'number') o.jumpSurviveVy = 0; + if (o.jumpSurviveOnGround == null) o.jumpSurviveOnGround = false; + if (typeof o.jumpSurviveNextJumpAi !== 'number') o.jumpSurviveNextJumpAi = Date.now() + 300 + Math.floor(Math.random() * 500); + if (typeof o.jumpSurviveWishNext !== 'number') o.jumpSurviveWishNext = Date.now() + 200; + if (o.jumpSurviveWishX == null) o.jumpSurviveWishX = 0; + + let pxc = (o.x + cw * 0.5) * ts; + let feetY = (o.y + ch) * ts; + + const camCy = jumpSurviveCamCenterY; + const mission = isJumpSurviveMissionUiMapPlay(); + const lowDanger = feetY > camCy + halfViewH - ts * 8.5; + + if (Date.now() >= o.jumpSurviveWishNext) { + const wMin = mission && lowDanger ? 180 : 380; + const wSpan = mission && lowDanger ? 480 : 700; + o.jumpSurviveWishNext = Date.now() + wMin + Math.floor(Math.random() * wSpan); + const tier = o.botTier || 'avg'; + if (mission && lowDanger && Math.random() < 0.55) { + o.jumpSurviveWishX = 0; + } else if (pxc < centerPx - ts * 1.8) o.jumpSurviveWishX = 1; + else if (pxc > centerPx + ts * 1.8) o.jumpSurviveWishX = -1; + else if (tier === 'sharp') { + const roam = mission ? 0.42 : 0.55; + o.jumpSurviveWishX = Math.random() < roam ? (Math.random() < 0.5 ? -1 : 1) : 0; + } else { + const roam = mission ? 0.28 : 0.4; + o.jumpSurviveWishX = Math.random() < roam ? (Math.random() < 0.5 ? -1 : 1) : 0; + } + } + + let moveLeft = o.jumpSurviveWishX < 0; + let moveRight = o.jumpSurviveWishX > 0; + let hazardJump = false; + if (o.jumpSurviveOnGround) { + const sc = jumpSurvivePlatformScrollPx; + if (moveRight && !jumpSurviveBotHasFloorAhead(md, pxc, feetY, 1, sc)) { + o.jumpSurviveWishX = Math.random() < 0.65 ? -1 : 0; + hazardJump = true; + } else if (moveLeft && !jumpSurviveBotHasFloorAhead(md, pxc, feetY, -1, sc)) { + o.jumpSurviveWishX = Math.random() < 0.65 ? 1 : 0; + hazardJump = true; + } + moveLeft = o.jumpSurviveWishX < 0; + moveRight = o.jumpSurviveWishX > 0; + } + + let jumpQ = false; + if (hazardJump) o.jumpSurviveNextJumpAi = Math.min(o.jumpSurviveNextJumpAi, Date.now()); + if (o.jumpSurviveOnGround && Date.now() >= o.jumpSurviveNextJumpAi) { + o.jumpSurviveNextJumpAi = Date.now() + 300 + Math.floor(Math.random() * (hazardJump ? 520 : 1100)); + const tier = o.botTier || 'avg'; + let jp = tier === 'sharp' ? 0.5 : tier === 'weak' ? 0.25 : 0.36; + if (mission) jp += 0.12; + if (lowDanger) jp += 0.22; + jp = Math.min(0.9, jp); + if (hazardJump || Math.random() < jp) jumpQ = true; + } + + const r = jumpSurvivePhysicsIntegrate(md, { + dt, + cw, + ch, + pxc0: pxc, + feetY0: feetY, + vy0: o.jumpSurviveVy, + wasOnGround: o.jumpSurviveOnGround, + jumpQueued: jumpQ, + moveLeft, + moveRight, + }); + + if (r.died) { + o.jumpSurviveEliminated = true; + o.jumpSurviveVy = 0; + jumpSurviveEntityFromHitbox(o, md, r.pxc, r.feetY); + o.tx = o.x; + o.ty = o.y; + o.botIsWalking = false; + return; + } + o.jumpSurviveVy = r.vy; + o.jumpSurviveOnGround = r.onGround; + jumpSurviveEntityFromHitbox(o, md, r.pxc, r.feetY); + o.tx = o.x; + o.ty = o.y; + if (moveRight) o.direction = 'right'; + else if (moveLeft) o.direction = 'left'; + o.botIsWalking = Math.abs(r.vy) > 40 || moveLeft || moveRight; + }); + } + + function jumpSurviveTickFrame() { + if (!mapData || !isJumpSurvive()) return; + if (isJumpSurviveMissionUiMapPlay()) { + if (jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) return; + const remEnd = jumpSurviveRemainingSecMission(); + if (remEnd !== null && remEnd <= 0) { + endJumpSurviveMissionRound('time_up'); + return; + } + } + const md = mapData; + const ts = tileSize; + const now = performance.now(); + const dt = Math.min(0.05, Math.max(0, (now - jumpSurviveLastTickMs) / 1000)); + jumpSurviveLastTickMs = now; + + let zJumpSurviveWorld = zoom; + if (previewMode && editorEmbedReturn && mapData) zJumpSurviveWorld *= playEmbedUserZoomMul; + const halfViewH = canvas.height / (2 * zJumpSurviveWorld); + const rise = (Number(md.jumpSurviveRisePxPerSec) > 0 ? Number(md.jumpSurviveRisePxPerSec) : 42) * dt; + jumpSurvivePlatformScrollPx += rise; + + const camTgt = jumpSurviveCameraTargetPx(md); + const ck = Math.min(1, dt * 7); + jumpSurviveCamCenterX += (camTgt.px - jumpSurviveCamCenterX) * ck; + jumpSurviveCamCenterY += (camTgt.py - jumpSurviveCamCenterY) * ck; + + if (jumpSurviveEliminated) { + stepJumpSurvivePreviewBots(dt, halfViewH); + jumpSurviveMissionMaybeEarlyFinish(); + return; + } + + const { cw, ch } = getCharacterFootprintWH(md); + let pxc = (me.x + cw * 0.5) * ts; + let feetY = (me.y + ch) * ts; + + const jq = jumpSurviveJumpQueued; + jumpSurviveJumpQueued = false; + + const r = jumpSurvivePhysicsIntegrate(md, { + dt, + cw, + ch, + pxc0: pxc, + feetY0: feetY, + vy0: jumpSurviveVy, + wasOnGround: jumpSurviveOnGround, + jumpQueued: jq, + moveLeft: !!(keys['ArrowLeft'] || keys['KeyA']), + moveRight: !!(keys['ArrowRight'] || keys['KeyD']), + }); + + if (r.died) { + jumpSurviveEliminated = true; + jumpSurviveVy = 0; + jumpSurviveSyncMeFromHitbox(r.pxc, r.feetY); + ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'KeyA', 'KeyW', 'KeyS', 'KeyD'].forEach((c) => { keys[c] = false; }); + } else { + jumpSurviveVy = r.vy; + jumpSurviveOnGround = r.onGround; + jumpSurviveSyncMeFromHitbox(r.pxc, r.feetY); + } + + stepJumpSurvivePreviewBots(dt, halfViewH); + + if (!jumpSurviveEliminated) { + me.isWalking = Math.abs(jumpSurviveVy) > 40 || !!(keys['ArrowLeft'] || keys['KeyA'] || keys['ArrowRight'] || keys['KeyD']); + if (keys['ArrowRight'] || keys['KeyD']) me.direction = 'right'; + else if (keys['ArrowLeft'] || keys['KeyA']) me.direction = 'left'; + + if (Date.now() - jumpSurviveLastEmitT > 80) { + jumpSurviveLastEmitT = Date.now(); + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + } + } + + jumpSurviveMissionMaybeEarlyFinish(); + } + + function gridImageLibIdleUrlPlay(entry) { + if (entry == null) return ''; + if (typeof entry === 'string') return entry; + if (typeof entry === 'object' && entry.idle) return String(entry.idle); + return ''; + } + function gridImageLibHeldUrlPlay(entry) { + if (entry == null || typeof entry === 'string') return ''; + if (typeof entry === 'object' && entry.held && String(entry.held).length) return String(entry.held); + return ''; + } + + function normalizeGridImageCellsOnMap(md) { + if (!md) return; + const w = md.width || 20, h = md.height || 15; + if (!Array.isArray(md.gridImageLibrary)) md.gridImageLibrary = []; + md.gridImageLibrary = md.gridImageLibrary + .map((raw) => { + if (typeof raw === 'string' && raw.length > 0 && raw.length < 30000000) return { idle: raw, held: null }; + if (raw && typeof raw === 'object' && typeof raw.idle === 'string' && raw.idle.length > 0 && raw.idle.length < 30000000) { + let held = typeof raw.held === 'string' && raw.held.length > 0 && raw.held.length < 30000000 ? raw.held : null; + if (held && held === raw.idle) held = null; + return { idle: raw.idle, held }; + } + return null; + }) + .filter(Boolean); + const libLen = md.gridImageLibrary.length; + let placements = []; + if (Array.isArray(md.gridImageSprites) && md.gridImageSprites.length) { + placements = md.gridImageSprites.map((raw) => { + const i = Math.max(0, Math.floor(Number(raw.i))); + const x = Math.floor(Number(raw.x)); + const y = Math.floor(Number(raw.y)); + const ww = Math.max(1, Math.floor(Number(raw.w)) || 1); + const hh = Math.max(1, Math.floor(Number(raw.h)) || 1); + const rawB = raw.bindCarryOption; + const bn = typeof rawB === 'number' ? rawB : parseInt(String(rawB), 10); + const base = { i, x, y, w: ww, h: hh }; + if (Number.isFinite(bn) && bn >= 1 && bn <= QUIZ_CARRY_MAX_OPTION_SLOTS) base.bindCarryOption = bn; + return base; + }).filter((s) => Number.isFinite(s.i) && s.i >= 0 && s.i < libLen && + Number.isFinite(s.x) && Number.isFinite(s.y) && s.w >= 1 && s.h >= 1 && + s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0); + placements = placements.map((s) => { + let x = s.x, y = s.y, ww = s.w, hh = s.h; + if (x < 0) { ww += x; x = 0; } + if (y < 0) { hh += y; y = 0; } + if (x + ww > w) ww = w - x; + if (y + hh > h) hh = h - y; + const out = { i: s.i, x, y, w: Math.max(1, ww), h: Math.max(1, hh) }; + if (s.bindCarryOption != null) out.bindCarryOption = s.bindCarryOption; + return out; + }).filter((s) => s.x < w && s.y < h && s.x + s.w > 0 && s.y + s.h > 0); + } else { + const cells = md.gridImageCells; + if (Array.isArray(cells) && cells.length === h && libLen > 0) { + for (let yy = 0; yy < h; yy++) { + const row = cells[yy]; + if (!row) continue; + for (let xx = 0; xx < w; xx++) { + if (row[xx] == null) continue; + const iv = parseInt(row[xx], 10); + if (!Number.isFinite(iv) || iv < 0 || iv >= libLen) continue; + placements.push({ i: iv, x: xx, y: yy, w: 1, h: 1 }); + } + } + } + } + md.gridImagePlacements = placements; + const outCells = Array(h).fill(0).map(() => Array(w).fill(null)); + placements.forEach((sp) => { + for (let yy = sp.y; yy < sp.y + sp.h; yy++) { + for (let xx = sp.x; xx < sp.x + sp.w; xx++) { + if (yy >= 0 && yy < h && xx >= 0 && xx < w) outCells[yy][xx] = sp.i; + } + } + }); + md.gridImageCells = outCells; + } + + function loadMapGridImages() { + mapGridImageImgs = []; + mapGridImageHeldImgs = []; + if (!mapData || !Array.isArray(mapData.gridImageLibrary) || !mapData.gridImageLibrary.length) return; + mapData.gridImageLibrary.forEach((entry, i) => { + mapGridImageImgs[i] = null; + mapGridImageHeldImgs[i] = null; + const idle = gridImageLibIdleUrlPlay(entry); + if (!idle) return; + const im = new Image(); + im.onload = () => { try { draw(); } catch (e) {} }; + im.onerror = () => {}; + im.src = idle; + mapGridImageImgs[i] = im; + const held = gridImageLibHeldUrlPlay(entry); + if (held && held !== idle) { + const hm = new Image(); + hm.onload = () => { try { draw(); } catch (e) {} }; + hm.onerror = () => {}; + hm.src = held; + mapGridImageHeldImgs[i] = hm; + } + }); + } + + /** โซนตัวเลือกที่ทับสไปรต์มากที่สุด — quiz_carry สลับรูป idle/held · ถ้าไม่ทับเลยลองขยายขอบ 1 ช่อง (มาร์กเกอร์ชิดขอบสไปรต์) */ + function dominantQuizCarryOptionInSpriteRect(md, sp) { + if (!md || md.gameType !== 'quiz_carry' || !md.quizCarryOptionArea) return null; + const g = md.quizCarryOptionArea; + const mw = md.width || 20, mh = md.height || 15; + const bx = sp.x | 0, by = sp.y | 0, bw = sp.w | 0, bh = sp.h | 0; + const counts = new Map(); + function addOverlapRect(x0, y0, ww, hh) { + for (let yy = y0; yy < y0 + hh; yy++) { + if (yy < 0 || yy >= mh) continue; + const row = g[yy]; + if (!row) continue; + for (let xx = x0; xx < x0 + ww; xx++) { + if (xx < 0 || xx >= mw) continue; + const v = row[xx]; + const n = typeof v === 'number' ? v : parseInt(String(v), 10); + if (!Number.isFinite(n) || n < 1 || n > QUIZ_CARRY_MAX_OPTION_SLOTS) continue; + const idx = n - 1; + counts.set(idx, (counts.get(idx) || 0) + 1); + } + } + } + addOverlapRect(bx, by, bw, bh); + if (!counts.size) { + const pad = 1; + const x0 = Math.max(0, bx - pad); + const y0 = Math.max(0, by - pad); + const x1 = Math.min(mw, bx + bw + pad); + const y1 = Math.min(mh, by + bh + pad); + addOverlapRect(x0, y0, x1 - x0, y1 - y0); + } + if (!counts.size) return null; + let best = null; + let bestN = -1; + counts.forEach((n, k) => { + if (best == null || n > bestN || (n === bestN && k < best)) { + bestN = n; + best = k; + } + }); + return best; + } + + /** 0-based choice index สำหรับสลับ idle/held — ใช้ bindCarryOption จากแมปก่อน แล้วค่อยนับทับโซน */ + function quizCarryOptionIndexForGridSprite(md, sp) { + if (!sp) return null; + const rawB = sp.bindCarryOption; + if (rawB != null && rawB !== '') { + const n = typeof rawB === 'number' ? rawB : parseInt(String(rawB), 10); + if (Number.isFinite(n) && n >= 1 && n <= QUIZ_CARRY_MAX_OPTION_SLOTS) return n - 1; + } + return dominantQuizCarryOptionInSpriteRect(md, sp); + } + + /** รูป held จากคลังกริดสำหรับข้อนี้ — ใช้วาดป้ายติดตัว (ไม่ผ่าน sanitize URL) */ + function findQuizCarryGridHeldImgForChoiceIndex(choiceIdx) { + if (choiceIdx == null || choiceIdx < 0 || !isQuizCarry() || !mapData) return null; + const pl = mapData.gridImagePlacements; + if (!Array.isArray(pl) || !pl.length) return null; + for (let gi = 0; gi < pl.length; gi++) { + const sp = pl[gi]; + const opt = quizCarryOptionIndexForGridSprite(mapData, sp); + if (opt !== choiceIdx) continue; + const gh = mapGridImageHeldImgs[sp.i]; + if (gh && gh.complete && gh.naturalWidth) return gh; + } + return null; + } + + /** รูป idle จากคลังกริดสำหรับข้อนี้ — วาดบนป้ายตัวเลือกบนพื้นเมื่อไม่มีรูปจากคำถาม/ธีม */ + function findQuizCarryGridIdleImgForChoiceIndex(choiceIdx) { + if (choiceIdx == null || choiceIdx < 0 || !isQuizCarry() || !mapData) return null; + const pl = mapData.gridImagePlacements; + if (!Array.isArray(pl) || !pl.length) return null; + for (let gi = 0; gi < pl.length; gi++) { + const sp = pl[gi]; + const opt = quizCarryOptionIndexForGridSprite(mapData, sp); + if (opt !== choiceIdx) continue; + const gid = mapGridImageImgs[sp.i]; + if (gid && gid.complete && gid.naturalWidth) return gid; + } + return null; + } + + function applyMapAndStart(gameMapData, res) { + const prevWasSpaceShooter = mapData && mapData.gameType === 'space_shooter'; + const prevWasBalloonBoss = mapData && mapData.gameType === 'balloon_boss'; + mapData = gameMapData; + playEmbedUserZoomMul = 1; + if (mapData.gameType !== 'quiz_carry') { + quizCarryWalkSpeedMultActive = QUIZ_CARRY_WALK_SPEED_MULT; + } + { + const qMap = (playMapIdFromQuery || '').trim(); + const dataId = mapData && mapData.id != null && String(mapData.id).trim() !== '' ? String(mapData.id).trim() : ''; + const resMap = res && res.mapId != null && String(res.mapId).trim() !== '' ? String(res.mapId).trim() : ''; + if (qMap) playSessionMapId = qMap; + else if (dataId) playSessionMapId = dataId; + else if (resMap) playSessionMapId = resMap; + } + applyPlayEmbedZoomForCurrentMapPlay(); + if (res && res.hostId != null) playHostId = res.hostId; + if (!isQuizQuestionMissionUiMapPlay()) { + quizQuestionMissionPhase = null; + quizQuestionMissionDeferredPhase = null; + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + } + if (mapData.gameType !== 'space_shooter') { + spaceShooterGameEnded = false; + if (prevWasSpaceShooter) { + const gend = document.getElementById('gauntlet-ended-overlay'); + if (gend) gend.classList.add('is-hidden'); + spaceShooterMissionPhase = null; + spaceShooterAsteroidExplosions = []; + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + const gccLeave = document.getElementById('gauntlet-crown-countdown'); + if (gccLeave) gccLeave.classList.add('is-hidden'); + } + } + if (mapData.gameType !== 'balloon_boss') { + balloonBossGameEnded = false; + if (prevWasBalloonBoss) { + const gend = document.getElementById('gauntlet-ended-overlay'); + if (gend) gend.classList.add('is-hidden'); + } + } + if (!mapData.lanes && mapData.gameType === 'frogger') mapData.lanes = []; + if (!mapData.interactive) mapData.interactive = []; + if (mapData.gameType === 'quiz') { + if (!mapData.quizTrueArea) mapData.quizTrueArea = []; + if (!mapData.quizFalseArea) mapData.quizFalseArea = []; + if (!mapData.quizQuestionArea) mapData.quizQuestionArea = []; + } + if (mapData.gameType === 'stack') { + if (!mapData.stackReleaseArea) mapData.stackReleaseArea = []; + if (!mapData.stackLandArea) mapData.stackLandArea = []; + } + if (mapData.gameType === 'quiz_carry') { + if (!mapData.quizCarryHubArea) mapData.quizCarryHubArea = []; + if (!mapData.quizCarryOptionArea) mapData.quizCarryOptionArea = []; + if (!mapData.carryEmbedCountdownArea) mapData.carryEmbedCountdownArea = []; + if (!mapData.quizQuestions) mapData.quizQuestions = []; + if (!mapData.quizQuestionArea) mapData.quizQuestionArea = []; + normalizeQuizCarryLayersInPlay(mapData); + applyQuizCarryWalkSpeedFromSettingsObj(quizCarryJoinSettingsSnap || {}); + } + if (mapData.gameType === 'quiz_battle') { + if (!mapData.quizBattleDomeArea) mapData.quizBattleDomeArea = []; + if (!mapData.quizBattlePathArea) mapData.quizBattlePathArea = []; + normalizeQuizBattlePathInPlay(mapData); + normalizeQuizBattleDomeInPlay(mapData); + } + tileSize = mapData.tileSize || 32; + mapBackgroundImg = null; + if (mapData.backgroundImage) { + mapBackgroundImg = new Image(); + mapBackgroundImg.src = mapData.backgroundImage; + } + reloadPlayScrollBgFromMap(); + reloadStackTowerScrollBgFromMap(); + reloadGauntletCrownRunwayBgFromMap(); + normalizeGridImageCellsOnMap(mapData); + loadMapGridImages(); + var modeLabel = isFrogger() ? ' | โหมดกบข้ามถนน' + : (isGauntlet() ? ' | พรมแดงสุดท้าย (Last Light)' + : (isLobby() ? ' | โถงรอ' : (isQuiz() ? ' | ตอบคำถาม' : (isQuizCarry() ? ' | หยิบคำตอบมาวางกลาง' : (isQuizBattle() ? ' | Quiz Battle · โดม E' : (isStack() ? ' | Stack ซ้อนตึก' : (isJumpSurvive() ? ' | กระโดดให้รอด' : (isSpaceShooter() ? ' | ยิงยานอวกาศ' : (isBalloonBoss() ? ' | ลูกโป้งยิงบอส' : ''))))))))); + var prevTag = previewMode ? '[ทดสอบ] ' : ''; + document.getElementById('room-id').textContent = prevTag + spaceId + modeLabel; + const plist = Array.isArray(res.peers) ? res.peers : []; + const myPeer = plist.find(p => p.id === myId); + const myPos = peerXYFromJoin(myPeer, mapData.spawn); + me.x = myPos.x; + me.y = myPos.y; + if (isQuizCarry()) { + const hubSnap = snapPositionOutOfQuizCarryHubIfNeeded(me.x, me.y); + me.x = hubSnap.x; + me.y = hubSnap.y; + } + if (isQuizBattle()) { + const pathSnap = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = pathSnap.x; + me.y = pathSnap.y; + } + me.direction = myPeer?.direction ?? 'down'; + me.characterId = myPeer?.characterId || null; + meGauntletJumpTicks = typeof myPeer?.gauntletJumpTicks === 'number' ? myPeer.gauntletJumpTicks : 0; + meGauntletJumpVis = meGauntletJumpTicks; + { + const sc0 = Number(myPeer && myPeer.gauntletScore); + me.gauntletScore = Number.isFinite(sc0) ? Math.max(0, sc0) : 0; + me.gauntletEliminated = !!(myPeer && myPeer.gauntletEliminated); + } + { + const sh0 = Number(myPeer && myPeer.spaceShooterScore); + me.spaceShooterScore = Number.isFinite(sh0) ? Math.max(0, sh0) : 0; + } + { + const bSt = balloonBossBalloonsStartPlay(); + const bs0 = Number(myPeer && myPeer.balloonBossScore); + me.balloonBossScore = Number.isFinite(bs0) ? Math.max(0, bs0) : 0; + const bd0 = Number(myPeer && myPeer.balloonBossBossDmg); + me.balloonBossBossDmg = Number.isFinite(bd0) ? Math.max(0, bd0) : 0; + const bb0 = Number(myPeer && myPeer.balloonBossBalloons); + me.balloonBossBalloons = Number.isFinite(bb0) ? Math.max(0, Math.floor(bb0)) : bSt; + me.balloonBossEliminated = !!(myPeer && myPeer.balloonBossEliminated); + } + gauntletObstacles = []; + gauntletObsRenderPrev = []; + gauntletObsRenderNext = []; + gauntletObsBlendT0 = 0; + me.playTint = playTintFromPeerId(String((myId != null && myId !== '') ? myId : (nick || 'local'))); + { + const jo = Number(myPeer && myPeer.spawnJoinOrder); + me.spawnJoinOrder = Number.isFinite(jo) ? Math.max(0, Math.floor(jo)) : Math.max(0, plist.findIndex((q) => q && q.id === myId)); + } + others.clear(); + plist.forEach(p => { + if (p.id !== myId) { + const pos = peerXYFromJoin(p, mapData.spawn); + let px = pos.x, py = pos.y; + if (isQuizCarry()) { + const hubSnap = snapPositionOutOfQuizCarryHubIfNeeded(px, py); + px = hubSnap.x; + py = hubSnap.y; + } + if (isQuizBattle()) { + const pathSnap = snapPositionOntoQuizBattlePathIfNeeded(px, py); + px = pathSnap.x; + py = pathSnap.y; + } + const joP = Number(p && p.spawnJoinOrder); + const spawnOrd = Number.isFinite(joP) ? Math.max(0, Math.floor(joP)) : Math.max(0, plist.findIndex((q) => q && q.id === p.id)); + others.set(p.id, { + x: px, y: py, tx: px, ty: py, direction: p.direction, nickname: p.nickname, characterId: p.characterId, + spawnJoinOrder: spawnOrd, + playTint: playTintFromPeerId(p.id), + gauntletJumpTicks: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0, + gauntletJumpVis: typeof p.gauntletJumpTicks === 'number' ? p.gauntletJumpTicks : 0, + gauntletScore: (() => { const s = Number(p.gauntletScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + gauntletEliminated: !!p.gauntletEliminated, + jumpSurviveEliminated: false, + spaceShooterScore: (() => { const s = Number(p.spaceShooterScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossScore: (() => { const s = Number(p.balloonBossScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossBossDmg: (() => { const s = Number(p.balloonBossBossDmg); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossBalloons: (() => { + const s = Number(p.balloonBossBalloons); + return Number.isFinite(s) ? Math.max(0, Math.floor(s)) : balloonBossBalloonsStartPlay(); + })(), + balloonBossEliminated: !!p.balloonBossEliminated, + quizCarryHeld: null, + }); + } + }); + if (mapData.gameType === 'gauntlet') { + me.tx = me.x; + me.ty = me.y; + if (res.gauntletEndsAt != null) { + const geJoin = Number(res.gauntletEndsAt); + gauntletEndsAtMs = Number.isFinite(geJoin) ? geJoin : null; + } else { + gauntletEndsAtMs = null; + } + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => {}); + } else { + me.tx = null; + me.ty = null; + gauntletEndsAtMs = null; + } + rebalancePreviewBots(); + if (mapData.gameType === 'space_shooter') { + applySpaceShooterSpawnLayoutPlay(); + } + if (mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + if (isStack()) { + lastStackTickMs = performance.now(); + resetStackMinigameState(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + else reapplyStackMiniSizingFromGlobals(); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => { + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }); + if (isStackTowerMissionUiMapPlay()) { + stackTowerMissionEndedOnce = false; + stackTowerMissionDeferredPhase = null; + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + stackTowerMissionPhase = 'howto'; + stackTowerSessionStartAt = 0; + hideConflictingOverlaysForGauntletCrown(); + applyStackTowerMissionPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (t) { if (t) applyGauntletTimingFromServer(t); }) + .catch(function () {}); + setTimeout(function () { showStackTowerMissionHowtoOverlay(); }, 50); + } else { + teardownStackTowerMissionUiPlay(); + } + } else { + stackMini = null; + teardownStackTowerMissionUiPlay(); + } + playPath = []; + me.isWalking = false; + if (mapData.gameType === 'jump_survive') { + if (!Array.isArray(mapData.jumpSurvivePlatforms)) mapData.jumpSurvivePlatforms = []; + if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = []; + if (!mapData.jumpSurviveHazardArea) mapData.jumpSurviveHazardArea = []; + if (!mapData.jumpSurvivePlatformVariantArea) mapData.jumpSurvivePlatformVariantArea = []; + normalizeJumpSurvivePlatformAreaInPlay(mapData); + normalizeJumpSurvivePlatformVariantAreaInPlay(mapData); + normalizeJumpSurviveHazardAreaInPlay(mapData); + normalizeShooterSpawnSlotsInPlay(mapData); + jumpSurviveEliminated = false; + jumpSurviveGameEnded = false; + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + if (isJumpSurviveMissionUiMapPlay()) { + jumpSurviveMissionPhase = 'howto'; + hideConflictingOverlaysForJumpSurviveMission(); + applyJumpSurviveJumperPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (t) { if (t) applyGauntletTimingFromServer(t); }) + .catch(function () {}); + setTimeout(function () { showJumpSurviveMissionHowtoOverlay(); }, 50); + } else { + jumpSurviveMissionPhase = null; + jumpSurviveInitRuntime(); + } + } + if (mapData.gameType === 'space_shooter') { + normalizeShooterSpawnSlotsInPlay(mapData); + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (t) { if (t) applyGauntletTimingFromServer(t); }) + .catch(function () {}); + if (isSpaceShooterMissionUiMapPlay()) { + spaceShooterMissionPhase = 'howto'; + hideConflictingOverlaysForGauntletCrown(); + applySpaceShooterMissionPanelImages(); + setTimeout(function () { showSpaceShooterMissionHowtoOverlay(); }, 50); + } else { + spaceShooterMissionPhase = null; + spaceShooterGameEnded = false; + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterAsteroidExplosions = []; + spaceShooterPopups = []; + spaceShooterLastTickMs = performance.now(); + spaceShooterSpawnAccMs = 0; + spaceShooterFireCd = 0; + spaceShooterSessionStartMs = performance.now(); + spaceShooterLastMoveEmit = 0; + } + } + if (mapData.gameType === 'balloon_boss') { + balloonBossGameEnded = false; + normalizeBalloonBossPlayerSlotsInPlay(mapData); + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + balloonBossHitFx = []; + balloonBossScorePopups = []; + balloonBossLastTickMs = performance.now(); + balloonBossPlayerFireCd = 0; + balloonBossSessionStartMs = performance.now(); + balloonBossLastMoveEmit = 0; + balloonBossBossFireAcc = 0; + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (t) { if (t) applyGauntletTimingFromServer(t); }) + .catch(function () {}); + } + if (isQuiz()) { + setupPlayQuizUi(); + if (isQuizQuestionMissionUiMapPlay()) { + quizQuestionMissionDeferredPhase = null; + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + quizQuestionMissionPhase = 'howto'; + hideConflictingOverlaysForGauntletCrown(); + applyQuizQuestionMissionPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (t) { if (t) applyGauntletTimingFromServer(t); }) + .catch(function () {}); + setTimeout(function () { showQuizQuestionMissionHowtoOverlay(); }, 50); + } + } else if (isQuizCarry()) setupPlayQuizCarryUi(); + else if (isQuizBattle()) setupPlayQuizBattleUi(); + else teardownPlayQuizUi(); + if (mapData.gameType === 'gauntlet' && isGauntletCrownHeistMapPlay()) { + hideConflictingOverlaysForGauntletCrown(); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccR = document.getElementById('gauntlet-crown-countdown'); + if (gccR) gccR.classList.add('is-hidden'); + setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 50); + } else if (mapData.gameType === 'balloon_boss' && isMegaVirusMissionShellMapPlay()) { + hideConflictingOverlaysForGauntletCrown(); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccRm = document.getElementById('gauntlet-crown-countdown'); + if (gccRm) gccRm.classList.add('is-hidden'); + if (res && Object.prototype.hasOwnProperty.call(res, 'gauntletCrownRunHeld')) { + applyGauntletTimingFromServer({ gauntletCrownRunHeld: res.gauntletCrownRunHeld }); + } + setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 50); + } else if (mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay()) { + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccRj = document.getElementById('gauntlet-crown-countdown'); + if (gccRj) gccRj.classList.add('is-hidden'); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + } else if (mapData.gameType === 'space_shooter' && isSpaceShooterMissionUiMapPlay()) { + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + const gccRs = document.getElementById('gauntlet-crown-countdown'); + if (gccRs) gccRs.classList.add('is-hidden'); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + } else if (isQuiz() && isQuizQuestionMissionUiMapPlay()) { + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + const gccRq = document.getElementById('gauntlet-crown-countdown'); + if (gccRq) gccRq.classList.add('is-hidden'); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + } else if (isStack() && isStackTowerMissionUiMapPlay()) { + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + const gccRst = document.getElementById('gauntlet-crown-countdown'); + if (gccRst) gccRst.classList.add('is-hidden'); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + } else { + const hov = document.getElementById('gauntlet-crown-howto-overlay'); + if (hov) { + hov.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + } + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccR2 = document.getElementById('gauntlet-crown-countdown'); + if (gccR2) gccR2.classList.add('is-hidden'); + teardownStackTowerMissionUiPlay(); + } + resizeCanvas(); draw(); tick(); + } + + socket.on('connect', () => { + firstCharacterDefaultPromise.then(() => { + const joinPlayMapId = (params.get('map') || '').trim(); + socket.emit('join-space', { + spaceId, + nickname: nick, + characterId: getPlayCharacterId(), + playMapId: joinPlayMapId || undefined, + }, (res) => { + if (!res || !res.ok) { + const errMsg = (res && res.error) || 'เข้าร่วมไม่ได้'; + alert(errMsg); + const caseLocked = /เริ่มคดี|ไม่รับผู้เล่น/.test(errMsg); + if (caseLocked) { + window.location.replace(buildRoomLobbyReturnHref()); + } else { + window.location.replace(BASE + '/lobby.html'); + } + return; + } + myId = socket.id; + let botSlots = parseInt(res.botSlotCount, 10); + if ((!botSlots || botSlots < 1) && res.maxPlayers > 0 && res.maxPlayers < 6) { + botSlots = 6 - res.maxPlayers; + } + if (botSlots > 0) { + detectiveCaseFillBots = true; + detectiveBotSlotCount = Math.min(6, Math.max(0, botSlots)); + } + if (isDetectiveMinigamePlay()) { + try { document.documentElement.classList.add('play-detective-minigame'); } catch (eD) { /* ignore */ } + } + if (res.quizSettingsSnap && typeof res.quizSettingsSnap === 'object') { + if (res.quizSettingsSnap.quizMapPanelTheme && typeof res.quizSettingsSnap.quizMapPanelTheme === 'object') { + setQuizMapPanelThemeFromApi({ quizMapPanelTheme: res.quizSettingsSnap.quizMapPanelTheme }); + } + } + if (res.quizCarrySettingsSnap && typeof res.quizCarrySettingsSnap === 'object') { + quizCarryJoinSettingsSnap = res.quizCarrySettingsSnap; + if (res.quizCarrySettingsSnap.carryMapPanelTheme && typeof res.quizCarrySettingsSnap.carryMapPanelTheme === 'object') { + setQuizCarryMapPanelThemeFromApi({ carryMapPanelTheme: res.quizCarrySettingsSnap.carryMapPanelTheme }); + } + if (res.quizCarrySettingsSnap.carryEmbedCountdownTheme && typeof res.quizCarrySettingsSnap.carryEmbedCountdownTheme === 'object') { + setQuizCarryEmbedCountdownThemeFromApi({ carryEmbedCountdownTheme: res.quizCarrySettingsSnap.carryEmbedCountdownTheme }); + } + if (Array.isArray(res.quizCarrySettingsSnap.carryChoicePlaqueThemes) && res.quizCarrySettingsSnap.carryChoicePlaqueThemes.length) { + setQuizCarryChoicePlaqueThemeFromApi({ carryChoicePlaqueThemes: res.quizCarrySettingsSnap.carryChoicePlaqueThemes }); + } else if (res.quizCarrySettingsSnap.carryChoicePlaqueTheme && typeof res.quizCarrySettingsSnap.carryChoicePlaqueTheme === 'object') { + setQuizCarryChoicePlaqueThemeFromApi({ carryChoicePlaqueTheme: res.quizCarrySettingsSnap.carryChoicePlaqueTheme }); + } + const joinSc = Number(res.quizCarrySettingsSnap.carryChoicePlaqueMapScale); + if (Number.isFinite(joinSc)) { + quizCarryPlaqueMapScale = Math.max(0.85, Math.min(2.5, joinSc)); + } + } + if (previewMode && editorEmbedReturn && res.mapData && res.mapData.gameType === 'lobby') { + const q = []; + q.push('space=' + encodeURIComponent(spaceId)); + q.push('nick=' + encodeURIComponent(nick)); + const mid = params.get('map'); + if (mid) q.push('map=' + encodeURIComponent(mid)); + q.push('preview=1'); + q.push('editorEmbed=1'); + if (params.get('defaultChar') != null) q.push('defaultChar=' + encodeURIComponent(params.get('defaultChar'))); + window.location.replace(BASE + '/room-lobby.html?' + q.join('&')); + return; + } + const playMapId = params.get('map'); + if (playMapId) { + fetch(BASE + '/api/maps/' + encodeURIComponent(playMapId)) + .then(r => r.ok ? r.json() : null) + .then(data => { + if (data) applyMapAndStart(data, res); + else applyMapAndStart(res.mapData, res); + }) + .catch(() => applyMapAndStart(res.mapData, res)); + } else { + applyMapAndStart(res.mapData, res); + } + }); + }); + }); + + const LERP = 0.2; + socket.on('user-joined', (data) => { + if (!data || isPreviewBotId(data.id)) return; + const x = Number(data.x); + const y = Number(data.y); + const px = Number.isFinite(x) ? x : 1; + const py = Number.isFinite(y) ? y : 1; + const joJ = Number(data && data.spawnJoinOrder); + const spawnOrdJ = Number.isFinite(joJ) ? Math.max(0, Math.floor(joJ)) : 0; + others.set(data.id, { + x: px, y: py, tx: px, ty: py, + direction: data.direction || 'down', nickname: data.nickname, + characterId: data.characterId ?? null, + spawnJoinOrder: spawnOrdJ, + playTint: playTintFromPeerId(data.id), + gauntletJumpTicks: typeof data.gauntletJumpTicks === 'number' ? data.gauntletJumpTicks : 0, + gauntletJumpVis: typeof data.gauntletJumpTicks === 'number' ? data.gauntletJumpTicks : 0, + gauntletScore: (() => { const s = Number(data.gauntletScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + jumpSurviveEliminated: false, + spaceShooterScore: (() => { const s = Number(data.spaceShooterScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossScore: (() => { const s = Number(data.balloonBossScore); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossBossDmg: (() => { const s = Number(data.balloonBossBossDmg); return Number.isFinite(s) ? Math.max(0, s) : 0; })(), + balloonBossBalloons: (() => { + const s = Number(data.balloonBossBalloons); + return Number.isFinite(s) ? Math.max(0, Math.floor(s)) : balloonBossBalloonsStartPlay(); + })(), + balloonBossEliminated: !!data.balloonBossEliminated, + quizCarryHeld: null, + }); + if (playBotsEnabled()) rebalancePreviewBots(); + if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud(); + if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud(); + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') updateStackTowerMissionHowtoHud(); + if (isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto') updateJumpSurviveMissionHowtoHud(); + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto') updateSpaceShooterMissionHowtoHud(); + }); + socket.on('host-changed', (d) => { + if (d && d.hostId != null) playHostId = d.hostId; + if (quizCarryPregameActive && isQuizCarry()) updateQuizCarryPregameHud(); + if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud(); + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') updateStackTowerMissionHowtoHud(); + if (isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto') updateJumpSurviveMissionHowtoHud(); + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto') updateSpaceShooterMissionHowtoHud(); + }); + socket.on('quiz-carry-lobby-sync', (d) => { + if (!d || typeof d.readyMap !== 'object') return; + quizCarryLobbyReadyMap = { ...d.readyMap }; + if (quizCarryPregameActive && isQuizCarry()) { + quizCarrySyncGuestReadyIfNeeded(); + updateQuizCarryPregameHud(); + } + }); + socket.on('quiz-carry-lobby-started', () => { + if (!(previewMode && editorEmbedReturn && isQuizCarry())) return; + endQuizCarryEmbedPregameAndStart(); + }); + socket.on('gauntlet-crown-lobby-sync', (d) => { + if (!d || typeof d.readyMap !== 'object') return; + gauntletCrownLobbyReadyMap = { ...d.readyMap }; + if (gauntletCrownPregamePhase === 'howto') { + gauntletCrownSyncGuestReadyIfNeeded(); + updateGauntletCrownHowtoHud(); + } else if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') { + gauntletCrownSyncGuestReadyIfNeeded(); + updateQuizQuestionMissionHowtoHud(); + } else if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') { + gauntletCrownSyncGuestReadyIfNeeded(); + updateStackTowerMissionHowtoHud(); + } else if (isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto') { + gauntletCrownSyncGuestReadyIfNeeded(); + updateJumpSurviveMissionHowtoHud(); + } else if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto') { + gauntletCrownSyncGuestReadyIfNeeded(); + updateSpaceShooterMissionHowtoHud(); + } + }); + socket.on('gauntlet-crown-lobby-started', () => { + if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') { + beginQuizQuestionMissionCountdownThenRun(); + return; + } + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') { + beginStackTowerMissionCountdownThenRun(); + return; + } + if (isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto') { + beginJumpSurviveMissionCountdownThenRun(); + return; + } + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto') { + beginSpaceShooterMissionCountdownThenRun(); + return; + } + if (!usesCrownLobbyShellPlay()) return; + beginGauntletCrownCountdownThenRun(); + }); + socket.on('user-move', (data) => { + if (data && myId != null && data.id === myId) { + /* Gauntlet: ตำแหน่งอ้างอิงจาก gauntlet-sync + เลอร์ปเท่านั้น — ไม่งั้น echo จาก move จะสแนป me.x/y โดยไม่อัปเดต tx/ty ทำให้เลอร์ปผิดและชนเซิร์ฟเวอร์ไม่ตรง */ + if (mapData && mapData.gameType === 'gauntlet') { + return; + } + if (mapData && mapData.gameType === 'stack') { + return; + } + if (mapData && mapData.gameType === 'jump_survive') { + return; + } + if (mapData && mapData.gameType === 'space_shooter') { + if (data.spaceShooterScore != null) { + const s = Number(data.spaceShooterScore); + if (Number.isFinite(s)) me.spaceShooterScore = Math.max(0, s); + } + return; + } + if (mapData && mapData.gameType === 'balloon_boss') { + if (data.balloonBossScore != null) { + const s = Number(data.balloonBossScore); + if (Number.isFinite(s)) me.balloonBossScore = Math.max(0, s); + } + if (data.balloonBossBossDmg != null) { + const d = Number(data.balloonBossBossDmg); + if (Number.isFinite(d)) me.balloonBossBossDmg = Math.max(0, d); + } + if (data.balloonBossBalloons != null) { + const b = Number(data.balloonBossBalloons); + if (Number.isFinite(b)) me.balloonBossBalloons = Math.max(0, Math.floor(b)); + } + if (data.balloonBossEliminated != null) me.balloonBossEliminated = !!data.balloonBossEliminated; + return; + } + if (mapData && mapData.gameType === 'quiz_battle') { + if (data.x != null) { + const x = Number(data.x); + if (Number.isFinite(x)) me.x = x; + } + if (data.y != null) { + const y = Number(data.y); + if (Number.isFinite(y)) me.y = y; + } + const s = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = s.x; + me.y = s.y; + if (data.direction) me.direction = data.direction; + if (data.characterId != null) me.characterId = data.characterId; + playPath = []; + return; + } + if (data.x != null) { + const x = Number(data.x); + if (Number.isFinite(x)) me.x = x; + } + if (data.y != null) { + const y = Number(data.y); + if (Number.isFinite(y)) me.y = y; + } + if (data.direction) me.direction = data.direction; + if (data.characterId != null) me.characterId = data.characterId; + playPath = []; + return; + } + const o = others.get(data.id); + if (o) { + if (mapData && mapData.gameType === 'quiz_battle' && quizBattlePathModeActive(mapData) && !isPreviewBotId(data.id)) { + const px = Number(data.x); + const py = Number(data.y); + if (Number.isFinite(px) && Number.isFinite(py)) { + const sn = snapPositionOntoQuizBattlePathIfNeeded(px, py); + o.tx = sn.x; + o.ty = sn.y; + o.x = sn.x; + o.y = sn.y; + } else { + o.tx = data.x; + o.ty = data.y; + } + } else { + o.tx = data.x; + o.ty = data.y; + } + o.direction = data.direction || o.direction; + if (data.characterId != null) o.characterId = data.characterId; + if (mapData && mapData.gameType === 'space_shooter' && data.spaceShooterScore != null) { + const s = Number(data.spaceShooterScore); + if (Number.isFinite(s)) o.spaceShooterScore = Math.max(0, s); + } + if (mapData && mapData.gameType === 'balloon_boss') { + if (data.balloonBossScore != null) { + const s = Number(data.balloonBossScore); + if (Number.isFinite(s)) o.balloonBossScore = Math.max(0, s); + } + if (data.balloonBossBossDmg != null) { + const d = Number(data.balloonBossBossDmg); + if (Number.isFinite(d)) o.balloonBossBossDmg = Math.max(0, d); + } + if (data.balloonBossBalloons != null) { + const b = Number(data.balloonBossBalloons); + if (Number.isFinite(b)) o.balloonBossBalloons = Math.max(0, Math.floor(b)); + } + if (data.balloonBossEliminated != null) o.balloonBossEliminated = !!data.balloonBossEliminated; + } + } + }); + socket.on('user-left', (data) => { + if (!data || isPreviewBotId(data.id)) return; + others.delete(data.id); + if (playBotsEnabled()) rebalancePreviewBots(); + if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase === 'howto') updateQuizQuestionMissionHowtoHud(); + if (isStackTowerMissionUiMapPlay() && stackTowerMissionPhase === 'howto') updateStackTowerMissionHowtoHud(); + if (isJumpSurviveMissionUiMapPlay() && jumpSurviveMissionPhase === 'howto') updateJumpSurviveMissionHowtoHud(); + if (isSpaceShooterMissionUiMapPlay() && spaceShooterMissionPhase === 'howto') updateSpaceShooterMissionHowtoHud(); + }); + socket.on('chat', (data) => { + const box = document.getElementById('chat-messages'); + if (!box) return; + const div = document.createElement('div'); + div.className = 'chat-msg'; + div.textContent = (data.nickname || '') + ': ' + (data.text || ''); + box.appendChild(div); + box.scrollTop = 1e9; + }); + + socket.on('quiz-phase', (p) => { + if (previewMode) return; + if (!p || !mapData || !isQuiz()) return; + if (isQuizQuestionMissionUiMapPlay() && quizQuestionMissionPhase !== 'live' && quizQuestionMissionPhase !== 'ended') { + quizQuestionMissionDeferredPhase = p; + return; + } + applyQuizPhaseFromServer(p); + }); + + socket.on('quiz-result', (r) => { + if (previewMode || !r) return; + if (r.scores) { + playLiveQuizScores = { ...r.scores }; + renderPlayQuizScoreboard(playLiveQuizScores); + } + if (isQuiz() && Array.isArray(r.results)) { + r.results.forEach(function (row) { + if (!row || row.id == null) return; + if (!row.right) playQuizEverWrong[String(row.id)] = true; + }); + } + if (isQuiz() && Array.isArray(r.results) && r.results.some(function (row) { return row && row.right; })) { + spawnQuizTrueFalseScorePopupPlay(!!r.correctTrue); + } + showPlayQuizFeedback(r); + }); + + socket.on('quiz-player-state', (st) => { + if (previewMode) return; + if (!st) return; + playQuizPlayerLocal = { + cannotTrue: !!st.cannotTrue, + cannotFalse: !!st.cannotFalse, + eliminated: !!st.eliminated, + score: typeof st.score === 'number' ? st.score : (playQuizPlayerLocal.score || 0), + }; + }); + + socket.on('quiz-ended', () => { + if (previewMode && isQuiz()) return; + let questionMissionEnd = null; + if (isQuizQuestionMissionUiMapPlay()) { + questionMissionEnd = quizQuestionMissionBuildPayload(); + } + playQuizPlayerLocal = { cannotTrue: false, cannotFalse: false, eliminated: false, score: 0 }; + playQuizPhaseLocal = null; + if (playQuizTimerInterval) { + clearInterval(playQuizTimerInterval); + playQuizTimerInterval = null; + } + const ov = document.getElementById('quiz-game-overlay'); + if (ov) ov.classList.add('is-hidden'); + const panel = document.getElementById('quiz-map-question-panel'); + if (panel) { + panel.classList.add('is-hidden'); + panel.setAttribute('aria-hidden', 'true'); + } + playQuizText = ''; + playQuizPhaseEndsAt = 0; + playLiveQuizScores = {}; + hidePlayQuizHud(); + if (questionMissionEnd) { + quizQuestionMissionPhase = 'ended'; + quizQuestionMissionDeferredPhase = null; + applyQuizQuestionMissionPanelImages(); + showGauntletCrownMissionOverlay(questionMissionEnd); + } + }); + + socket.on('gauntlet-sync', (data) => { + if (!data || typeof data !== 'object') return; + if (isMegaVirusMissionShellMapPlay()) { + applyGauntletTimingFromServer(data); + return; + } + if (!mapData || mapData.gameType !== 'gauntlet') return; + applyGauntletTimingFromServer(data); + if (Array.isArray(data.obstacles)) { + gauntletObstacles = data.obstacles; + pushGauntletObsRenderFrame(data.obstacles); + } + if (!Array.isArray(data.players)) return; + data.players.forEach((p) => { + if (!p || p.id == null || myId == null) return; + const pid = String(p.id); + const mid = String(myId); + if (pid === mid) { + const prevSc = Math.max(0, Number(me.gauntletScore) || 0); + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) me.tx = px; + if (Number.isFinite(py)) me.ty = py; + if (p.direction) me.direction = p.direction; + const jt = Number(p.gauntletJumpTicks); + meGauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + meGauntletJumpVis = meGauntletJumpTicks; + const sc = Number(p.gauntletScore); + const newSc = Number.isFinite(sc) ? Math.max(0, sc) : 0; + me.gauntletScore = newSc; + if (isGauntletCrownHeistMapPlay() && prevSc - newSc >= 10) { + me.gauntletCrownPenaltyFxUntil = Date.now() + 1100; + } + me.gauntletEliminated = !!p.gauntletEliminated; + clampGauntletCrownHeistEntityXInPlacePlay(me); + } else { + const o = others.get(p.id) ?? others.get(pid); + if (o) { + const prevSc = Math.max(0, Number(o.gauntletScore) || 0); + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) o.tx = px; + if (Number.isFinite(py)) o.ty = py; + if (p.direction) o.direction = p.direction; + const jt = Number(p.gauntletJumpTicks); + o.gauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks; + const sc = Number(p.gauntletScore); + const newSc = Number.isFinite(sc) ? Math.max(0, sc) : 0; + o.gauntletScore = newSc; + if (isGauntletCrownHeistMapPlay() && prevSc - newSc >= 10) { + o.gauntletCrownPenaltyFxUntil = Date.now() + 1100; + } + o.gauntletEliminated = !!p.gauntletEliminated; + clampGauntletCrownHeistEntityXInPlacePlay(o); + } + } + }); + if (playBotsEnabled()) { + applyGauntletPreviewBotsAfterSync(); + emitGauntletPreviewRowsToServer(); + } + }); + + socket.on('gauntlet-ended', (data) => { + cancelQuizCarryResultEndAfterTimeup(); + gauntletEndsAtMs = null; + gauntletCrownRunwayBgFinishLatched = false; + gauntletCrownRunwayBgStripFreezeSinceMs = 0; + gauntletCrownRunwayClientMissionShown = false; + gauntletCrownRunHeldRemote = false; + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + resetGauntletCrownRunwaySpawnScrollSnap(); + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc = document.getElementById('gauntlet-crown-countdown'); + if (gcc) gcc.classList.add('is-hidden'); + const hto = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto) hto.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + if (data && data.crownMission) { + showGauntletCrownMissionOverlay(data.crownMission); + return; + } + const ov = document.getElementById('gauntlet-ended-overlay'); + const msgEl = document.getElementById('gauntlet-ended-message'); + const titleEl = document.getElementById('gauntlet-ended-title'); + const listEl = document.getElementById('gauntlet-ended-rankings'); + const btn = document.getElementById('btn-gauntlet-ended-lobby'); + if (!ov || !msgEl || !listEl) return; + if (titleEl) { + titleEl.textContent = data && data.reason === 'time' ? 'หมดเวลา · Time up' : 'เกมจบ · Game over'; + } + msgEl.textContent = (data && data.message) || 'เกมพรมแดงจบแล้ว'; + listEl.innerHTML = ''; + const ranks = (data && data.rankings) || []; + ranks.forEach((r, i) => { + const li = document.createElement('li'); + const isMe = myId != null && r && String(r.id) === String(myId); + li.textContent = `${i + 1}. ${(r && r.nickname) || '—'} — ${Math.max(0, Number(r && r.score) || 0)}`; + if (isMe) li.className = 'gauntlet-ended-me'; + listEl.appendChild(li); + }); + ov.classList.remove('is-hidden'); + function goLobby() { + window.location.href = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + } + if (btn) { + btn.onclick = () => { + if (previewMode && editorEmbedReturn) ov.classList.add('is-hidden'); + else goLobby(); + }; + } + }); + + socket.on('game-start', (ev) => { + if (!ev || !ev.mapId) return; + const urlMap = (params.get('map') || '').trim(); + if (urlMap && ev.stayInRoomLobby && String(ev.mapId).trim() !== urlMap) { + return; + } + const applySnap = (md) => { + mapData = md; + playEmbedUserZoomMul = 1; + if (ev.mapId != null && String(ev.mapId).trim() !== '') { + playSessionMapId = String(ev.mapId).trim(); + } + applyPlayEmbedZoomForCurrentMapPlay(); + if (!isQuizQuestionMissionUiMapPlay()) { + quizQuestionMissionPhase = null; + quizQuestionMissionDeferredPhase = null; + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + } + if (!isStackTowerMissionUiMapPlay()) { + teardownStackTowerMissionUiPlay(); + } + if (mapData.gameType !== 'space_shooter') { + spaceShooterGameEnded = false; + spaceShooterMissionPhase = null; + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + } + if (mapData.gameType !== 'balloon_boss') { + balloonBossGameEnded = false; + } + if (!mapData.lanes && mapData.gameType === 'frogger') mapData.lanes = []; + tileSize = mapData.tileSize || 32; + gauntletObstacles = []; + gauntletObsRenderPrev = []; + gauntletObsRenderNext = []; + gauntletObsBlendT0 = 0; + mapBackgroundImg = null; + if (mapData.backgroundImage) { + mapBackgroundImg = new Image(); + mapBackgroundImg.src = mapData.backgroundImage; + } + reloadPlayScrollBgFromMap(); + reloadStackTowerScrollBgFromMap(); + reloadGauntletCrownRunwayBgFromMap(); + normalizeGridImageCellsOnMap(mapData); + loadMapGridImages(); + if (mapData.gameType === 'gauntlet' && ev.peersSnap && Array.isArray(ev.peersSnap)) { + ev.peersSnap.forEach((p) => { + if (p.id != null && myId != null && String(p.id) === String(myId)) { + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) { me.x = px; me.tx = px; } + if (Number.isFinite(py)) { me.y = py; me.ty = py; } + me.direction = p.direction || me.direction; + const jt = Number(p.gauntletJumpTicks); + meGauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + meGauntletJumpVis = meGauntletJumpTicks; + const sc = Number(p.gauntletScore); + me.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0; + me.gauntletEliminated = !!p.gauntletEliminated; + } else { + const o = others.get(p.id) ?? others.get(String(p.id)); + if (o) { + const px = Number(p.x); + const py = Number(p.y); + if (Number.isFinite(px)) { o.x = px; o.tx = px; } + if (Number.isFinite(py)) { o.y = py; o.ty = py; } + o.direction = p.direction || o.direction; + const jt = Number(p.gauntletJumpTicks); + o.gauntletJumpTicks = Number.isFinite(jt) ? jt : 0; + o.gauntletJumpVis = o.gauntletJumpTicks; + const sc = Number(p.gauntletScore); + o.gauntletScore = Number.isFinite(sc) ? Math.max(0, sc) : 0; + o.gauntletEliminated = !!p.gauntletEliminated; + } + } + }); + } else { + meGauntletJumpTicks = 0; + meGauntletJumpVis = 0; + me.gauntletScore = 0; + me.gauntletEliminated = false; + others.forEach((o) => { + o.gauntletJumpTicks = 0; + o.gauntletJumpVis = 0; + o.gauntletScore = 0; + o.gauntletEliminated = false; + }); + } + if (mapData.gameType === 'gauntlet') { + if (ev.gauntletEndsAt != null) { + const ge = Number(ev.gauntletEndsAt); + gauntletEndsAtMs = Number.isFinite(ge) ? ge : null; + } else { + gauntletEndsAtMs = null; + } + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => {}); + if (String(ev.mapId || '') === GAUNTLET_FACE_RIGHT_MAP_ID) { + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc0 = document.getElementById('gauntlet-crown-countdown'); + if (gcc0) gcc0.classList.add('is-hidden'); + setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 60); + } else { + const hto2 = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto2) hto2.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc1 = document.getElementById('gauntlet-crown-countdown'); + if (gcc1) gcc1.classList.add('is-hidden'); + } + } else { + gauntletEndsAtMs = null; + const megaShellGs = mapData.gameType === 'balloon_boss' && String(ev.mapId || '').trim() === BALLOON_BOSS_MISSION_MAP_ID; + const skipHideCrownShell = (mapData.gameType === 'jump_survive' && isJumpSurviveMissionUiMapPlay()) + || (mapData.gameType === 'space_shooter' && isSpaceShooterMissionUiMapPlay()) + || (mapData.gameType === 'quiz' && isQuizQuestionMissionUiMapPlay()) + || (mapData.gameType === 'stack' && isStackTowerMissionUiMapPlay()) + || megaShellGs; + if (!skipHideCrownShell) { + const hto3 = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto3) hto3.classList.add('is-hidden'); + gauntletCrownHowtoVisible = false; + } + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gcc2 = document.getElementById('gauntlet-crown-countdown'); + if (gcc2) gcc2.classList.add('is-hidden'); + } + if (mapData.gameType !== 'gauntlet') { + me.tx = null; + me.ty = null; + } + const gauntletOv = document.getElementById('gauntlet-ended-overlay'); + if (gauntletOv) gauntletOv.classList.add('is-hidden'); + const gcmOv = document.getElementById('gauntlet-crown-mission-overlay'); + if (gcmOv) gcmOv.classList.add('is-hidden'); + if (mapData.gameType === 'stack') { + if (!mapData.stackReleaseArea) mapData.stackReleaseArea = []; + if (!mapData.stackLandArea) mapData.stackLandArea = []; + rebalancePreviewBots(); + lastStackTickMs = performance.now(); + resetStackMinigameState(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + else reapplyStackMiniSizingFromGlobals(); + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }) + .catch(() => { + if (stackMini && isStack()) stackMini.phaseSpeed = playStackSwingHz; + }); + if (isStackTowerMissionUiMapPlay()) { + stackTowerMissionEndedOnce = false; + stackTowerMissionDeferredPhase = null; + if (stackTowerMissionCountdownTimer) { + clearTimeout(stackTowerMissionCountdownTimer); + stackTowerMissionCountdownTimer = null; + } + stackTowerMissionPhase = 'howto'; + stackTowerSessionStartAt = 0; + hideConflictingOverlaysForGauntletCrown(); + applyStackTowerMissionPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + setTimeout(function () { showStackTowerMissionHowtoOverlay(); }, 60); + } else { + teardownStackTowerMissionUiPlay(); + } + } else { + stackMini = null; + teardownStackTowerMissionUiPlay(); + } + if (mapData.gameType === 'jump_survive') { + if (!Array.isArray(mapData.jumpSurvivePlatforms)) mapData.jumpSurvivePlatforms = []; + if (!mapData.jumpSurvivePlatformArea) mapData.jumpSurvivePlatformArea = []; + if (!mapData.jumpSurviveHazardArea) mapData.jumpSurviveHazardArea = []; + if (!mapData.jumpSurvivePlatformVariantArea) mapData.jumpSurvivePlatformVariantArea = []; + normalizeJumpSurvivePlatformAreaInPlay(mapData); + normalizeJumpSurvivePlatformVariantAreaInPlay(mapData); + normalizeJumpSurviveHazardAreaInPlay(mapData); + normalizeShooterSpawnSlotsInPlay(mapData); + jumpSurviveEliminated = false; + jumpSurviveGameEnded = false; + if (jumperMissionCountdownTimer) { + clearTimeout(jumperMissionCountdownTimer); + jumperMissionCountdownTimer = null; + } + if (isJumpSurviveMissionUiMapPlay()) { + jumpSurviveMissionPhase = 'howto'; + hideConflictingOverlaysForJumpSurviveMission(); + applyJumpSurviveJumperPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + setTimeout(function () { showJumpSurviveMissionHowtoOverlay(); }, 60); + } else { + jumpSurviveMissionPhase = null; + jumpSurviveInitRuntime(); + } + } + if (mapData.gameType === 'space_shooter') { + normalizeShooterSpawnSlotsInPlay(mapData); + if (spaceShooterMissionCountdownTimer) { + clearTimeout(spaceShooterMissionCountdownTimer); + spaceShooterMissionCountdownTimer = null; + } + if (isSpaceShooterMissionUiMapPlay()) { + spaceShooterMissionPhase = 'howto'; + hideConflictingOverlaysForGauntletCrown(); + applySpaceShooterMissionPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + setTimeout(function () { showSpaceShooterMissionHowtoOverlay(); }, 60); + } else { + spaceShooterMissionPhase = null; + spaceShooterGameEnded = false; + spaceShooterBullets = []; + spaceShooterAsteroids = []; + spaceShooterAsteroidExplosions = []; + spaceShooterPopups = []; + spaceShooterLastTickMs = performance.now(); + spaceShooterSpawnAccMs = 0; + spaceShooterFireCd = 0; + spaceShooterSessionStartMs = performance.now(); + spaceShooterLastMoveEmit = 0; + } + } + if (mapData.gameType === 'quiz') { + if (!mapData.quizTrueArea) mapData.quizTrueArea = []; + if (!mapData.quizFalseArea) mapData.quizFalseArea = []; + if (!mapData.quizQuestionArea) mapData.quizQuestionArea = []; + } + if (mapData.gameType === 'balloon_boss') { + balloonBossGameEnded = false; + normalizeBalloonBossPlayerSlotsInPlay(mapData); + balloonBossPendingShots = []; + balloonBossPlayerBullets = []; + balloonBossBossBullets = []; + balloonBossHitFx = []; + balloonBossScorePopups = []; + balloonBossLastTickMs = performance.now(); + balloonBossPlayerFireCd = 0; + balloonBossLastMoveEmit = 0; + balloonBossBossFireAcc = 0; + me.balloonBossScore = 0; + me.balloonBossBossDmg = 0; + me.balloonBossBalloons = balloonBossBalloonsStartPlay(); + me.balloonBossEliminated = false; + others.forEach((o) => { + o.balloonBossScore = 0; + o.balloonBossBossDmg = 0; + o.balloonBossBalloons = balloonBossBalloonsStartPlay(); + o.balloonBossEliminated = false; + }); + if (String(ev.mapId || '').trim() === BALLOON_BOSS_MISSION_MAP_ID) { + balloonBossSessionStartMs = 0; + if (Object.prototype.hasOwnProperty.call(ev, 'gauntletCrownRunHeld')) { + applyGauntletTimingFromServer({ gauntletCrownRunHeld: ev.gauntletCrownRunHeld }); + } + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { + if (t) applyGauntletTimingFromServer(t); + if (Object.prototype.hasOwnProperty.call(ev, 'gauntletCrownRunHeld')) { + applyGauntletTimingFromServer({ gauntletCrownRunHeld: ev.gauntletCrownRunHeld }); + } + }) + .catch(() => {}); + gauntletCrownPregamePhase = null; + gauntletCrownLobbyReadyMap = {}; + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + const gccMv = document.getElementById('gauntlet-crown-countdown'); + if (gccMv) gccMv.classList.add('is-hidden'); + setTimeout(function () { showGauntletCrownHowtoOverlay(); }, 60); + } else { + balloonBossSessionStartMs = performance.now(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + } + } + if (mapData.gameType === 'quiz_battle') { + if (!mapData.quizBattleDomeArea) mapData.quizBattleDomeArea = []; + if (!mapData.quizBattlePathArea) mapData.quizBattlePathArea = []; + normalizeQuizBattlePathInPlay(mapData); + normalizeQuizBattleDomeInPlay(mapData); + if (quizBattlePathModeActive(mapData)) { + const ms = snapPositionOntoQuizBattlePathIfNeeded(me.x, me.y); + me.x = ms.x; + me.y = ms.y; + others.forEach((o) => { + const s = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y); + o.x = s.x; + o.y = s.y; + }); + } + } + var modeLabel = isFrogger() ? ' | โหมดกบข้ามถนน' + : (isGauntlet() ? ' | พรมแดงสุดท้าย (Last Light)' + : (isLobby() ? ' | โถงรอ' : (isQuiz() ? ' | ตอบคำถาม' : (isQuizCarry() ? ' | หยิบคำตอบมาวางกลาง' : (isQuizBattle() ? ' | Quiz Battle · โดม E' : (isStack() ? ' | Stack ซ้อนตึก' : (isJumpSurvive() ? ' | กระโดดให้รอด' : (isSpaceShooter() ? ' | ยิงยานอวกาศ' : (isBalloonBoss() ? ' | ลูกโป้งยิงบอส' : ''))))))))); + var prevTag = previewMode ? '[ทดสอบ] ' : ''; + document.getElementById('room-id').textContent = prevTag + spaceId + modeLabel; + if (isQuiz()) { + setupPlayQuizUi(); + if (isQuizQuestionMissionUiMapPlay()) { + quizQuestionMissionDeferredPhase = null; + if (quizQuestionMissionCountdownTimer) { + clearTimeout(quizQuestionMissionCountdownTimer); + quizQuestionMissionCountdownTimer = null; + } + quizQuestionMissionPhase = 'howto'; + hideConflictingOverlaysForGauntletCrown(); + applyQuizQuestionMissionPanelImages(); + fetch(BASE + '/api/game-timing?_=' + Date.now(), { cache: 'no-store' }) + .then((r) => (r.ok ? r.json() : null)) + .then((t) => { if (t) applyGauntletTimingFromServer(t); }) + .catch(() => {}); + setTimeout(function () { showQuizQuestionMissionHowtoOverlay(); }, 60); + } + } else if (isQuizCarry()) setupPlayQuizCarryUi(); + else if (isQuizBattle()) setupPlayQuizBattleUi(); + else teardownPlayQuizUi(); + resizeCanvas(); + if (mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + if (playBotsEnabled() && mapData.gameType === 'gauntlet') { + applyGauntletPreviewSpawnLayout(true); + emitGauntletPreviewRowsToServer(); + } + if (playBotsEnabled() && mapData.gameType === 'jump_survive') { + applyJumpSurvivePreviewSpawnLayout(true); + } + if (playBotsEnabled() && mapData.gameType === 'space_shooter') { + applySpaceShooterSpawnLayoutPlay(); + } + if (playBotsEnabled() && mapData.gameType === 'balloon_boss') { + applyBalloonBossSpawnLayoutPlay(); + } + }; + fetch(BASE + '/api/maps/' + encodeURIComponent(ev.mapId)) + .then((r) => (r.ok ? r.json() : null)) + .then((md) => { if (md) applySnap(md); }); + }); + + function resizeCanvas() { + if (mapData && isStackTowerMissionUiMapPlay() && canvas) { + canvas.width = Math.max(320, STACK_TOWER_FIXED_RENDER_W); + canvas.height = Math.max(240, STACK_TOWER_FIXED_RENDER_H); + /* รีคำนวน BG scroll หลังขนาดบัฟเฟอร์คงที่ — กันรูปโหลดเร็วตอนแคนวาสยังไม่ล็อกแล้วไม่ซิงก์ใหม่ */ + stackTowerScrollBgSrawWorldBaselinePlay = null; + stackTowerScrollBgSyncInitialScrollToBottom(); + syncQuizCarryEmbedCountdownLayout(); + return; + } + const vw = window.innerWidth || document.documentElement.clientWidth || 800; + const vh = window.innerHeight || document.documentElement.clientHeight || 600; + const header = document.querySelector('.game-header'); + const headerH = header && header.offsetHeight ? header.offsetHeight : 48; + const stage = document.getElementById('play-canvas-stage'); + const stackEl = document.getElementById('play-canvas-stack'); + const parent = stage || (canvas && canvas.parentElement); + let cw = canvas.clientWidth || 0; + let ch = canvas.clientHeight || 0; + if (parent && parent.clientWidth > 80) cw = Math.max(cw, parent.clientWidth); + if (parent && parent.clientHeight > 80) ch = Math.max(ch, parent.clientHeight); + /* จอใหญ่ / iframe embed: client บางทียัง 0 ช้ากว่า layout — ใช้ offset เป็นทางเลือก */ + if (parent) { + const ow = parent.offsetWidth || 0; + const oh = parent.offsetHeight || 0; + if (ow > 80) cw = Math.max(cw, ow); + if (oh > 80) ch = Math.max(ch, oh); + } + /* embed: stage อาจยัง 0 แต่ stack วัดได้แล้ว — อย่าให้ fallback เป็น vw/vh เต็มจอแล้ว CSS ย่อเข้า stage = ดูชิดมุม */ + if (previewMode && editorEmbedReturn && stackEl) { + const sw = Math.max(stackEl.clientWidth || 0, stackEl.offsetWidth || 0); + const sh = Math.max(stackEl.clientHeight || 0, stackEl.offsetHeight || 0); + if (sw > 80) cw = Math.max(cw, sw); + if (sh > 80) ch = Math.max(ch, sh); + } + if (ch < 120) ch = Math.max(240, vh - headerH - 6); + if (cw < 120) { + if (previewMode && editorEmbedReturn && stackEl) { + const sw = Math.max(stackEl.clientWidth || 0, stackEl.offsetWidth || 0); + cw = sw > 80 ? Math.max(320, sw) : Math.max(320, vw); + } else cw = Math.max(320, vw); + } + canvas.width = Math.max(320, cw); + canvas.height = Math.max(240, ch); + syncQuizCarryEmbedCountdownLayout(); + } + + canvas.addEventListener('click', (e) => { + if (!mapData || !isStack() || isChatFocused()) return; + tryHumanStackDrop(); + }); + (function setupPlayEditorEmbedViewZoom() { + const stack = document.getElementById('play-canvas-stack'); + if (!stack) return; + const wheelZoomHandler = (e) => { + if (!previewMode || !editorEmbedReturn || !mapData) return; + if (isStackTowerEmbedZoomLockedPlay()) return; + if (isQuizQuestionMissionHudActivePlay()) return; + const t = e.target; + if (t && typeof t.closest === 'function') { + if (t.closest('button, input, textarea, select, a[href]')) return; + } + /* ล้อธรรม / trackpad pinch มักไม่มี ctrl — ให้ซูมได้ใน embed; Ctrl/⌘+ล้อยังใช้ได้ */ + e.preventDefault(); + e.stopPropagation(); + const raw = e.deltaMode === 1 ? e.deltaY * 14 : e.deltaMode === 2 ? e.deltaY * 320 : e.deltaY; + const k = Math.exp(-raw * 0.002); + playEmbedUserZoomMul = Math.max( + PLAY_EMBED_USER_ZOOM_MIN, + Math.min(PLAY_EMBED_USER_ZOOM_MAX, playEmbedUserZoomMul * k), + ); + draw(); + }; + stack.addEventListener('wheel', wheelZoomHandler, { passive: false, capture: true }); + })(); + + canvas.addEventListener('dblclick', (e) => { + if (!mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss() || isChatFocused() || isQuizCarryEmbedCountdownBlockingMovement()) return; + const r = canvas.getBoundingClientRect(); + const sx = e.clientX - r.left; + const sy = e.clientY - r.top; + const qmC = isQuizQuestionMissionHudActivePlay() ? getQuizQuestionMissionMapCenterWorldPxPlay() : null; + const carryCam = !qmC && isQuizCarry() ? getQuizCarryMapCameraWorldCenterPxPlay() : null; + const camX = qmC ? qmC.cx : (carryCam ? carryCam.cx : me.x * tileSize); + const camY = qmC ? qmC.cy : (carryCam ? carryCam.cy : me.y * tileSize); + const zHit = lastPlayZDrawForInput > 0 ? lastPlayZDrawForInput : zoom; + const gx = (sx - canvas.width / 2) / zHit + camX; + const gy = (sy - canvas.height / 2) / zHit + camY; + const tx = Math.floor(gx / tileSize); + const ty = Math.floor(gy / tileSize); + const mw = mapData.width || 20, mh = mapData.height || 15; + if (tx < 0 || tx >= mw || ty < 0 || ty >= mh) return; + if (!canWalkLikeLobby(tx + 0.5, ty + 0.5)) return; + const path = pathfindPlay(me.x, me.y, tx + 0.5, ty + 0.5); + if (path.length <= 1) return; + playPath = path.slice(1); + }); + + function getCharacterFootprintWH(md) { + if (!md) return { cw: 1, ch: 1 }; + let cw = Math.floor(Number(md.characterCellsW)); + let ch = Math.floor(Number(md.characterCellsH)); + const hasW = Number.isFinite(cw) && cw >= 1; + const hasH = Number.isFinite(ch) && ch >= 1; + if (hasW && hasH) { + return { cw: Math.max(1, Math.min(4, cw)), ch: Math.max(1, Math.min(4, ch)) }; + } + const leg = Math.max(1, Math.min(4, Math.floor(Number(md.characterCells)) || 1)); + return { cw: leg, ch: leg }; + } + + /** + * ขนาดช่องที่ใช้ชนกำแพง (objects=1) เท่านั้น — ชิดเท้า (ล่าง) + กลางแนวนอน · เล็กกว่า footprint = ส่วนบนไม่ติดกำแพง + */ + function getCharacterCollisionFootprintWH(md) { + const { cw, ch } = getCharacterFootprintWH(md); + let colW = Math.floor(Number(md.characterCollisionW)); + let colH = Math.floor(Number(md.characterCollisionH)); + if (!Number.isFinite(colW) || colW < 1) colW = cw; + if (!Number.isFinite(colH) || colH < 1) colH = ch; + colW = Math.max(1, Math.min(cw, colW)); + colH = Math.max(1, Math.min(ch, colH)); + return { cw, ch, colW, colH }; + } + + function getGauntletCrownHeistMaxEntityXPlay(md) { + if (!md || !isGauntletCrownHeistMapPlay()) return null; + const w = md.width || 20; + const { cw } = getCharacterFootprintWH(md); + const capRight = GAUNTLET_CROWN_HEIST_MAX_X_WORLD_FRAC * w - cw; + const fullMapMax = Math.max(0, w - cw + 0.999); + return Math.max(0, Math.min(fullMapMax, capRight)); + } + function clampGauntletCrownHeistEntityXInPlacePlay(ent) { + if (!ent || !mapData || !isGauntletCrownHeistMapPlay()) return; + const mx = getGauntletCrownHeistMaxEntityXPlay(mapData); + if (mx == null || !Number.isFinite(mx)) return; + if (Number.isFinite(ent.x)) ent.x = Math.max(0, Math.min(mx, ent.x)); + if (ent.tx != null && Number.isFinite(ent.tx)) ent.tx = Math.max(0, Math.min(mx, ent.tx)); + } + + /** กันตัวละคร footprint ล้นออกนอกแมป — เดิมใช้ w-0.01 ทำให้ cw/ch>1 ชนขอบแล้วตรรกะเดินค้าง */ + function clampPlayEntityFootprintToMap(oEnt, md) { + if (!oEnt || !md) return; + const w = md.width || 20, h = md.height || 15; + const { cw, ch } = getCharacterFootprintWH(md); + let maxX = Math.max(0, w - cw + 0.999); + const crownMx = getGauntletCrownHeistMaxEntityXPlay(md); + if (crownMx != null) maxX = Math.min(maxX, crownMx); + const maxY = Math.max(0, h - ch + 0.999); + if (!Number.isFinite(oEnt.x)) oEnt.x = 0.5; + if (!Number.isFinite(oEnt.y)) oEnt.y = 0.5; + oEnt.x = Math.max(0, Math.min(maxX, oEnt.x)); + oEnt.y = Math.max(0, Math.min(maxY, oEnt.y)); + } + + /** แยกบอท preview ที่ทับกันบนจุดเดียว (มักชนกันที่ขอบแมป) */ + function separateClumpedPreviewBots() { + if (!playBotsEnabled() || !mapData) return; + const ids = [...others.keys()].filter(isPreviewBotId); + const minD = 0.52; + const pushAmt = MOVE_SPEED * 0.5; + for (let i = 0; i < ids.length; i++) { + for (let j = i + 1; j < ids.length; j++) { + const a = others.get(ids[i]); + const b = others.get(ids[j]); + if (!a || !b) continue; + let ddx = b.x - a.x, ddy = b.y - a.y; + let d = Math.sqrt(ddx * ddx + ddy * ddy); + if (d >= minD) continue; + if (d < 1e-5) { + ddx = (i + j * 2) % 2 === 0 ? 1 : -1; + ddy = ((i + j) % 3) - 1; + d = Math.sqrt(ddx * ddx + ddy * ddy) || 1; + } + const ux = -ddx / d, uy = -ddy / d; + const vx = ddx / d, vy = ddy / d; + const ax2 = a.x + ux * pushAmt, ay2 = a.y + uy * pushAmt; + const bx2 = b.x + vx * pushAmt, by2 = b.y + vy * pushAmt; + if (canWalkLikeLobbyForBot(ax2, ay2, a.x, a.y, a)) { a.x = ax2; a.y = ay2; } + if (canWalkLikeLobbyForBot(bx2, by2, b.x, b.y, b)) { b.x = bx2; b.y = by2; } + clampPlayEntityFootprintToMap(a, mapData); + clampPlayEntityFootprintToMap(b, mapData); + } + } + } + + function quizTilesFootprintPlay(px, py) { + const s = new Set(); + if (!mapData) return s; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s; + const { cw, ch } = getCharacterFootprintWH(mapData); + const w = mapData.width || 20, h = mapData.height || 15; + const minTx = Math.floor(px); + const minTy = Math.floor(py); + const maxTx = Math.min(w - 1, minTx + cw - 1); + const maxTy = Math.min(h - 1, minTy + ch - 1); + for (let ty = minTy; ty <= maxTy; ty++) { + for (let tx = minTx; tx <= maxTx; tx++) { + if (tx >= 0 && ty >= 0) s.add(tx + ',' + ty); + } + } + return s; + } + + /** ร่าง (cw×ch) ของผู้เล่นที่ (px,py) ครอบคลุมช่อง (tx,ty) หรือไม่ — ใช้กับ blockPlayer แทนการเช็คแค่มุมซ้ายบน */ + function playEntityFootprintContainsTile(px, py, tx, ty) { + return quizTilesFootprintPlay(px, py).has(tx + ',' + ty); + } + + /** + * กล่องตรวจโซนห้ามซ้อน (blockPlayer) — วางเหมือนชนกำแพง: กลางแนวนอน + ชิดเท้า + * blockPlayerSeparationW/H: 0 = ใช้เต็มความกว้าง/สูงตัว · ค่าอื่น = จำกัด 1..cw / 1..ch + * รองรับ legacy blockPlayerSeparation เมื่อยังไม่มี W/H ใน JSON + */ + function resolveBlockPlayerNoOverlapBoxWH(md) { + const m = md || mapData; + if (!m) return { cw: 1, ch: 1, boxW: 1, boxH: 1 }; + const { cw, ch } = getCharacterFootprintWH(m); + let w = m.blockPlayerSeparationW != null && m.blockPlayerSeparationW !== '' ? Math.floor(Number(m.blockPlayerSeparationW)) : NaN; + let h = m.blockPlayerSeparationH != null && m.blockPlayerSeparationH !== '' ? Math.floor(Number(m.blockPlayerSeparationH)) : NaN; + const leg = m.blockPlayerSeparation != null && m.blockPlayerSeparation !== '' ? Math.floor(Number(m.blockPlayerSeparation)) : NaN; + const hasW = Number.isFinite(w) && w >= 0; + const hasH = Number.isFinite(h) && h >= 0; + if (!hasW && !hasH && Number.isFinite(leg) && leg >= 1) { + w = Math.min(leg, cw); + h = Math.min(leg, ch); + } else { + if (!hasW) w = 0; + if (!hasH) h = 0; + } + const boxW = !w || w <= 0 ? cw : Math.max(1, Math.min(cw, w)); + const boxH = !h || h <= 0 ? ch : Math.max(1, Math.min(ch, h)); + return { cw, ch, boxW, boxH }; + } + + /** ช่องกริดที่ถือว่า “ผู้เล่นที่ (px,py) กินพื้นที่” สำหรับตรวจโซน blockPlayer — กล่องเดียวกับแนวชนกำแพง */ + function quizTilesBlockPlayerPeerFootprintPlay(px, py) { + const s = new Set(); + if (!mapData) return s; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s; + const { cw, ch, boxW, boxH } = resolveBlockPlayerNoOverlapBoxWH(mapData); + const w = mapData.width || 20, h = mapData.height || 15; + const minTx = Math.floor(px); + const minTy = Math.floor(py); + const offX = minTx + Math.floor((cw - boxW) / 2); + const offY = minTy + (ch - boxH); + for (let ty = offY; ty < offY + boxH; ty++) { + for (let tx = offX; tx < offX + boxW; tx++) { + if (tx >= 0 && ty >= 0 && tx < w && ty < h) s.add(tx + ',' + ty); + } + } + return s; + } + + function playPeerBlockPlayerOccupiesTile(px, py, tx, ty) { + return quizTilesBlockPlayerPeerFootprintPlay(px, py).has(tx + ',' + ty); + } + + /** Footprint ชนกำแพง (objects=1) เท่านั้น — hub / interactive / blockPlayer / quiz ใช้ quizTilesFootprintPlay = เต็ม characterCells */ + function quizTilesWallCollisionFootprintPlay(px, py) { + const s = new Set(); + if (!mapData) return s; + if (typeof px !== 'number' || typeof py !== 'number' || !Number.isFinite(px) || !Number.isFinite(py)) return s; + const { cw, ch, colW, colH } = getCharacterCollisionFootprintWH(mapData); + const w = mapData.width || 20, h = mapData.height || 15; + const minTx = Math.floor(px); + const minTy = Math.floor(py); + const offX = minTx + Math.floor((cw - colW) / 2); + const offY = minTy + (ch - colH); + for (let ty = offY; ty < offY + colH; ty++) { + for (let tx = offX; tx < offX + colW; tx++) { + if (tx >= 0 && ty >= 0 && tx < w && ty < h) s.add(tx + ',' + ty); + } + } + return s; + } + + function quizAnswerTileForbiddenForLock(lock, tx, ty) { + if (!lock || lock.eliminated) return false; + if (!mapData) return false; + const qt = mapData.quizTrueArea; + const qf = mapData.quizFalseArea; + if (lock.cannotTrue && qt && qt[ty] && qt[ty][tx] === 1) return true; + if (lock.cannotFalse && qf && qf[ty] && qf[ty][tx] === 1) return true; + return false; + } + + function quizAnswerTileForbiddenPlay(tx, ty) { + if (!playQuizPlayerLocal) return false; + return quizAnswerTileForbiddenForLock({ + cannotTrue: !!playQuizPlayerLocal.cannotTrue, + cannotFalse: !!playQuizPlayerLocal.cannotFalse, + eliminated: !!playQuizPlayerLocal.eliminated, + }, tx, ty); + } + + /** ยืนทับโซนต้องห้ามเมื่อโดนล็อก — ใช้กับ pathfind ปลายทาง / คลิก */ + function quizLockFootprintBlocksForLock(lock, px, py) { + if (!mapData || !isQuiz() || !lock || lock.eliminated) return false; + for (const k of quizTilesFootprintPlay(px, py)) { + const p = k.split(','); + const txi = +p[0], tyi = +p[1]; + if (quizAnswerTileForbiddenForLock(lock, txi, tyi)) return true; + } + return false; + } + + function quizLockFootprintBlocksPlay(px, py) { + if (!playQuizPlayerLocal) return false; + return quizLockFootprintBlocksForLock({ + cannotTrue: !!playQuizPlayerLocal.cannotTrue, + cannotFalse: !!playQuizPlayerLocal.cannotFalse, + eliminated: !!playQuizPlayerLocal.eliminated, + }, px, py); + } + + /** บล็อกเฉพาะการ «เข้า» ช่องตอบใหม่ — ให้เดินออกจากโซนได้ถ้าเคยยืนผิดแล้ว */ + function quizLockWouldEnterForbiddenForLock(lock, ox, oy, nx, ny) { + if (!mapData || !isQuiz() || !lock || lock.eliminated) return false; + const fromS = quizTilesFootprintPlay(ox, oy); + const toS = quizTilesFootprintPlay(nx, ny); + for (const k of toS) { + if (fromS.has(k)) continue; + const p = k.split(','); + const txi = +p[0], tyi = +p[1]; + if (quizAnswerTileForbiddenForLock(lock, txi, tyi)) return true; + } + return false; + } + + function quizLockWouldEnterForbiddenPlay(ox, oy, nx, ny) { + if (!playQuizPlayerLocal) return false; + return quizLockWouldEnterForbiddenForLock({ + cannotTrue: !!playQuizPlayerLocal.cannotTrue, + cannotFalse: !!playQuizPlayerLocal.cannotFalse, + eliminated: !!playQuizPlayerLocal.eliminated, + }, ox, oy, nx, ny); + } + + function botQuizLock(o) { + return { + cannotTrue: !!(o && o.quizCannotTrue), + cannotFalse: !!(o && o.quizCannotFalse), + eliminated: false, + }; + } + + /** Same walkability as room-lobby `canWalkLobby` (LobbyA / hall). */ + function canWalkLikeLobby(x, y, fromX, fromY) { + if (!mapData || !mapData.objects) return false; + if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false; + const w = mapData.width || 20, h = mapData.height || 15; + const wallTiles = quizTilesWallCollisionFootprintPlay(x, y); + for (const k of wallTiles) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = mapData.objects[ty]; + if (!row || row[tx] === 1) return false; + } + if (wallTiles.size === 0) return false; + const bp = mapData.blockPlayer; + if (bp) { + for (const k of quizTilesFootprintPlay(x, y)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (!bp[ty] || bp[ty][tx] !== 1) continue; + for (const [, o] of others) { + if (playPeerBlockPlayerOccupiesTile(o.x, o.y, tx, ty)) return false; + } + } + } + if (isQuiz() && playQuizPlayerLocal && !playQuizPlayerLocal.eliminated) { + const hasFrom = typeof fromX === 'number' && typeof fromY === 'number' && !Number.isNaN(fromX) && !Number.isNaN(fromY); + if (hasFrom) { + if (quizLockWouldEnterForbiddenPlay(fromX, fromY, x, y)) return false; + } else if (quizLockFootprintBlocksPlay(x, y)) { + return false; + } + } + if (isQuizCarry() && quizCarryFootprintOverlapsHub(x, y)) return false; + if (isQuizBattle() && quizBattlePathModeActive(mapData)) { + if (!quizBattleFootprintFullyOnPath(mapData, x, y)) return false; + } + return true; + } + + function canWalkLikeLobbyForBot(x, y, fromX, fromY, o) { + if (!mapData || !mapData.objects) return false; + if (typeof x !== 'number' || typeof y !== 'number' || !Number.isFinite(x) || !Number.isFinite(y)) return false; + const w = mapData.width || 20, h = mapData.height || 15; + const wallTilesB = quizTilesWallCollisionFootprintPlay(x, y); + for (const k of wallTilesB) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (tx < 0 || tx >= w || ty < 0 || ty >= h) return false; + const row = mapData.objects[ty]; + if (!row || row[tx] === 1) return false; + } + if (wallTilesB.size === 0) return false; + const bp = mapData.blockPlayer; + if (bp) { + for (const k of quizTilesFootprintPlay(x, y)) { + const p = k.split(','); + const tx = +p[0], ty = +p[1]; + if (!bp[ty] || bp[ty][tx] !== 1) continue; + for (const [, peer] of others) { + if (o && peer === o) continue; + if (playPeerBlockPlayerOccupiesTile(peer.x, peer.y, tx, ty)) return false; + } + if (o && playPeerBlockPlayerOccupiesTile(me.x, me.y, tx, ty)) return false; + } + } + if (isQuiz()) { + const lock = botQuizLock(o); + if (lock.cannotTrue || lock.cannotFalse) { + const hasFrom = typeof fromX === 'number' && typeof fromY === 'number' && !Number.isNaN(fromX) && !Number.isNaN(fromY); + if (hasFrom) { + if (quizLockWouldEnterForbiddenForLock(lock, fromX, fromY, x, y)) return false; + } else if (quizLockFootprintBlocksForLock(lock, x, y)) { + return false; + } + } + } + if (isQuizCarry() && quizCarryFootprintOverlapsHub(x, y)) return false; + if (isQuizBattle() && quizBattlePathModeActive(mapData)) { + if (!quizBattleFootprintFullyOnPath(mapData, x, y)) return false; + } + return true; + } + + /** ถ้าสปอว์น/โหลดมาทับโซน hub ให้หาจุดใกล้ ๆ ที่ footprint ไม่ทับ hub */ + function snapPositionOutOfQuizCarryHubIfNeeded(x, y) { + if (!mapData || !isQuizCarry()) return { x, y }; + if (!quizCarryFootprintOverlapsHub(x, y)) return { x, y }; + const w = mapData.width || 20, h = mapData.height || 15; + const maxR = Math.max(w, h) + 6; + for (let r = 1; r <= maxR; r++) { + for (let dy = -r; dy <= r; dy++) { + for (let dx = -r; dx <= r; dx++) { + if (Math.max(Math.abs(dx), Math.abs(dy)) !== r) continue; + const nx = x + dx * 0.45; + const ny = y + dy * 0.45; + if (nx < 0 || ny < 0 || nx > w - 0.02 || ny > h - 0.02) continue; + if (quizCarryFootprintOverlapsHub(nx, ny)) continue; + if (!canWalkLikeLobby(nx, ny, NaN, NaN)) continue; + return { x: nx, y: ny }; + } + } + } + return { x, y }; + } + + function stepPreviewBotAlongPath(o, w, h) { + const path = o.botPath; + if (!path || !path.length) return; + const way = path[0]; + const dx = way.x - o.x, dy = way.y - o.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist <= PATH_ARRIVE_THRESH) { + path.shift(); + while (path.length > 0) { + const w2 = path[0]; + const ux = w2.x - o.x, uy = w2.y - o.y; + if (Math.sqrt(ux * ux + uy * uy) > PATH_ARRIVE_THRESH) break; + path.shift(); + } + clampPlayEntityFootprintToMap(o, mapData); + return; + } + const len = dist || 1; + const carryWalk = isQuizCarry() ? quizCarryWalkSpeedMultActive : 1; + const baseEmbedCarryPathMul = 0.74; + const pathStepMul = (previewFillBots && editorEmbedReturn) + ? (isQuizCarry() ? baseEmbedCarryPathMul * (quizCarryWalkSpeedMultActive / QUIZ_CARRY_WALK_SPEED_MULT) : 0.56) + : 1; + const step = Math.min(MOVE_SPEED * 1.28 * pathStepMul * carryWalk, len); + const nx = o.x + (dx / len) * step; + const ny = o.y + (dy / len) * step; + if (Math.abs(dy) > Math.abs(dx)) o.direction = dy > 0 ? 'down' : 'up'; + else if (Math.abs(dx) > 1e-6) o.direction = dx > 0 ? 'right' : 'left'; + const ox = o.x, oy = o.y; + const pathStrict = isQuizBattle() && quizBattlePathModeActive(mapData); + if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) { + o.x = nx; + o.y = ny; + } else if (!pathStrict) { + if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) { + o.x = nx; + } else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) { + o.y = ny; + } else { + /* เส้นตรงไป waypoint อาจตัดมุม hub / block — ลองก้าวแนวแกนตามทิศหลักอย่างใดอย่างหนึ่ง */ + const cstep = Math.min(MOVE_SPEED * 1.2 * pathStepMul * carryWalk, Math.max(Math.abs(dx), Math.abs(dy), 1e-4)); + const sx = dx > 1e-4 ? 1 : dx < -1e-4 ? -1 : 0; + const sy = dy > 1e-4 ? 1 : dy < -1e-4 ? -1 : 0; + const tryAxisX = () => { + if (sx === 0) return false; + if (!canWalkLikeLobbyForBot(o.x + sx * cstep, o.y, o.x, o.y, o)) return false; + o.x += sx * cstep; + return true; + }; + const tryAxisY = () => { + if (sy === 0) return false; + if (!canWalkLikeLobbyForBot(o.x, o.y + sy * cstep, o.x, o.y, o)) return false; + o.y += sy * cstep; + return true; + }; + if (Math.abs(dx) >= Math.abs(dy)) { + if (!tryAxisX()) tryAxisY(); + } else { + if (!tryAxisY()) tryAxisX(); + } + } + } + clampPlayEntityFootprintToMap(o, mapData); + if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) { + o.botIsWalking = true; + o.botPathStuckTicks = 0; + } else { + o.botPathStuckTicks = (o.botPathStuckTicks | 0) + 1; + if ((o.botPathStuckTicks | 0) >= 20) { + o.botPathStuckTicks = 0; + if (path.length) path.shift(); + if (isQuizCarry() && playBotsEnabled()) o.botQuizCarryPathfindAfter = 0; + } + } + } + + function pickRandomPreviewBotWanderDir() { + const dirs = [[0, -1], [0, 1], [-1, 0], [1, 0]]; + return dirs[Math.floor(Math.random() * dirs.length)]; + } + + function stepPreviewBots() { + if (!playBotsEnabled() || !mapData || isFrogger() || isGauntlet() || isStack() || isJumpSurvive() || isSpaceShooter() || isBalloonBoss()) return; + if (isQuizCarry()) { + stepQuizCarryPreviewBots(); + return; + } + const w = mapData.width || 20, h = mapData.height || 15; + const now = Date.now(); + const inAnswerPhase = previewMode && isQuiz() && previewQuizStep === 'answer'; + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + o.botIsWalking = false; + if (inAnswerPhase && o.botPath && o.botPath.length > 0 && !o.botAnswerWander) { + stepPreviewBotAlongPath(o, w, h); + return; + } + if (inAnswerPhase && o.botPath && o.botPath.length === 0 && !o.botAnswerWander) { + return; + } + /* ก่อนตอบ / พัก / บอทสับสน: เดินทุกเฟรมตามทิศ (เหมือนผู้เล่นกดค้าง) — เดิมรอเป็นจังหวะแล้วก้าวทีละครั้งเลยดูกระตุก */ + if (o.botWanderDx == null || o.botWanderDy == null || (o.botWanderDx === 0 && o.botWanderDy === 0)) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + if (typeof o.botWanderNextTurn !== 'number') o.botWanderNextTurn = now + 600; + if (now >= o.botWanderNextTurn) { + o.botWanderNextTurn = now + 650 + Math.floor(Math.random() * 2200); + if (Math.random() < 0.55) { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + } + } + const accX = o.botWanderDx; + const accY = o.botWanderDy; + if (Math.abs(accY) > Math.abs(accX)) o.direction = accY > 0 ? 'down' : 'up'; + else if (accX !== 0) o.direction = accX > 0 ? 'right' : 'left'; + const step = MOVE_SPEED; + const nx = o.x + accX * step; + const ny = o.y + accY * step; + const ox = o.x, oy = o.y; + const pathStrictW = isQuizBattle() && quizBattlePathModeActive(mapData); + if (canWalkLikeLobbyForBot(nx, ny, o.x, o.y, o)) { + o.x = nx; + o.y = ny; + } else if (!pathStrictW) { + if (canWalkLikeLobbyForBot(nx, o.y, o.x, o.y, o)) { + o.x = nx; + } else if (canWalkLikeLobbyForBot(o.x, ny, o.x, o.y, o)) { + o.y = ny; + } else { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600); + } + } else { + const d = pickRandomPreviewBotWanderDir(); + o.botWanderDx = d[0]; + o.botWanderDy = d[1]; + o.botWanderNextTurn = now + 200 + Math.floor(Math.random() * 600); + } + if (!Number.isFinite(o.x)) o.x = 0.5; + if (!Number.isFinite(o.y)) o.y = 0.5; + clampPlayEntityFootprintToMap(o, mapData); + if (Math.abs(o.x - ox) > 1e-5 || Math.abs(o.y - oy) > 1e-5) o.botIsWalking = true; + }); + separateClumpedPreviewBots(); + [...others.keys()].filter(isPreviewBotId).forEach((bid) => { + const ob = others.get(bid); + if (ob) clampPlayEntityFootprintToMap(ob, mapData); + }); + if (isQuizBattle() && quizBattlePathModeActive(mapData)) { + others.forEach((o, id) => { + if (!isPreviewBotId(id)) return; + const s = snapPositionOntoQuizBattlePathIfNeeded(o.x, o.y); + o.x = s.x; + o.y = s.y; + o.tx = s.x; + o.ty = s.y; + }); + } + } + + /** A* เหมือน room-lobby — double-click ไปจุดบนแผนที่ */ + function pathfindPlay(fromX, fromY, toX, toY) { + if (!mapData) return []; + const w = mapData.width || 20, h = mapData.height || 15; + const fx = Math.floor(fromX), fy = Math.floor(fromY); + const tx = Math.floor(toX), ty = Math.floor(toY); + if (tx < 0 || tx >= w || ty < 0 || ty >= h || !canWalkLikeLobby(tx + 0.5, ty + 0.5)) return []; + if (fx === tx && fy === ty) return [{ x: tx + 0.5, y: ty + 0.5 }]; + const key = (gx, gy) => gx + ',' + gy; + const open = [{ gx: fx, gy: fy, f: 0, g: 0 }]; + const closed = new Set(); + const cameFrom = {}; + const gScore = { [key(fx, fy)]: 0 }; + const heuristic = (ax, ay) => Math.abs(ax - tx) + Math.abs(ay - ty); + const dirs = [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }]; + while (open.length) { + open.sort((a, b) => a.f - b.f); + const cur = open.shift(); + const ck = key(cur.gx, cur.gy); + if (closed.has(ck)) continue; + closed.add(ck); + if (cur.gx === tx && cur.gy === ty) { + const path = []; + let u = cur; + while (u) { + path.unshift({ x: u.gx + 0.5, y: u.gy + 0.5 }); + u = cameFrom[key(u.gx, u.gy)]; + } + return path; + } + for (const d of dirs) { + const nx = cur.gx + d.dx, ny = cur.gy + d.dy; + if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue; + if (!canWalkLikeLobby(nx + 0.5, ny + 0.5, cur.gx + 0.5, cur.gy + 0.5)) continue; + const nk = key(nx, ny); + if (closed.has(nk)) continue; + const g = (gScore[ck] ?? Infinity) + 1; + if (g >= (gScore[nk] ?? Infinity)) continue; + gScore[nk] = g; + cameFrom[nk] = cur; + open.push({ gx: nx, gy: ny, f: g + heuristic(nx, ny), g }); + } + } + return []; + } + + function pathfindPlayForBot(fromX, fromY, toX, toY, o) { + if (!mapData) return []; + const w = mapData.width || 20, h = mapData.height || 15; + const fx = Math.floor(fromX), fy = Math.floor(fromY); + const tx = Math.floor(toX), ty = Math.floor(toY); + if (tx < 0 || tx >= w || ty < 0 || ty >= h || !canWalkLikeLobbyForBot(tx + 0.5, ty + 0.5, NaN, NaN, o)) return []; + if (fx === tx && fy === ty) return [{ x: tx + 0.5, y: ty + 0.5 }]; + const key = (gx, gy) => gx + ',' + gy; + const open = [{ gx: fx, gy: fy, f: 0, g: 0 }]; + const closed = new Set(); + const cameFrom = {}; + const gScore = { [key(fx, fy)]: 0 }; + const heuristic = (ax, ay) => Math.abs(ax - tx) + Math.abs(ay - ty); + const dirs = [{ dx: 0, dy: -1 }, { dx: 1, dy: 0 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }]; + while (open.length) { + open.sort((a, b) => a.f - b.f); + const cur = open.shift(); + const ck = key(cur.gx, cur.gy); + if (closed.has(ck)) continue; + closed.add(ck); + if (cur.gx === tx && cur.gy === ty) { + const path = []; + let u = cur; + while (u) { + path.unshift({ x: u.gx + 0.5, y: u.gy + 0.5 }); + u = cameFrom[key(u.gx, u.gy)]; + } + return path; + } + for (const d of dirs) { + const nx = cur.gx + d.dx, ny = cur.gy + d.dy; + if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue; + if (!canWalkLikeLobbyForBot(nx + 0.5, ny + 0.5, cur.gx + 0.5, cur.gy + 0.5, o)) continue; + const nk = key(nx, ny); + if (closed.has(nk)) continue; + const g = (gScore[ck] ?? Infinity) + 1; + if (g >= (gScore[nk] ?? Infinity)) continue; + gScore[nk] = g; + cameFrom[nk] = cur; + open.push({ gx: nx, gy: ny, f: g + heuristic(nx, ny), g }); + } + } + return []; + } + + let playPath = []; + + function drawGauntletLaserColumnScreen(rx, ry, rw, rh) { + ctx.save(); + const topRec = gauntletLaserTopUrl ? ensureGauntletAssetImage(gauntletLaserTopUrl) : null; + const botRec = gauntletLaserBottomUrl ? ensureGauntletAssetImage(gauntletLaserBottomUrl) : null; + const lineRec = gauntletLaserLineUrl ? ensureGauntletAssetImage(gauntletLaserLineUrl) : null; + let topH = 0; + let botH = 0; + if (topRec && topRec.img.complete && topRec.img.naturalWidth > 0) { + topH = Math.min(rh * 0.4, rw * topRec.img.naturalHeight / topRec.img.naturalWidth); + } + if (botRec && botRec.img.complete && botRec.img.naturalWidth > 0) { + botH = Math.min(rh * 0.4, rw * botRec.img.naturalHeight / botRec.img.naturalWidth); + } + const lineReady = !!(lineRec && lineRec.img.complete && lineRec.img.naturalWidth > 0 && rh > 1); + /* ไม่เติมสีคอลัมน์ทึบเมื่อมีรูปเส้นแล้ว — กันทึบโปร่งซ้าย/ขวาเลเซอร์ดูเหมือนแถบ UI/scrollbar */ + if (!lineReady) { + ctx.fillStyle = gauntletLaserFillColor; + ctx.fillRect(rx, ry, rw, rh); + } + /* เส้นกลาง tile ทั้งความสูงคอลัมน์ แล้วค่อยวาดหัว/ท้ายทับ — ให้ลำแสงต่อเนื่องแบบสินทรัพย์รวม (รูปอ้างอิง) */ + if (lineReady) { + const iw = lineRec.img.naturalWidth; + const ih = lineRec.img.naturalHeight; + const scale = rw / iw; + const step = Math.max(1, ih * scale); + let y = ry; + while (y < ry + rh) { + const piece = Math.min(step, ry + rh - y); + const srcH = piece / scale; + try { + ctx.drawImage(lineRec.img, 0, 0, iw, srcH, rx, y, rw, piece); + } catch (e) { /* ignore */ } + y += piece; + } + } + if (topH > 0 && topRec) { + try { ctx.drawImage(topRec.img, rx, ry, rw, topH); } catch (e) { /* ignore */ } + } + if (botH > 0 && botRec) { + try { ctx.drawImage(botRec.img, rx, ry + rh - botH, rw, botH); } catch (e) { /* ignore */ } + } + const lw = Number(gauntletLaserLineWidthPx) || 0; + if (lw > 0) { + ctx.strokeStyle = gauntletLaserStrokeColor; + ctx.lineWidth = lw; + ctx.strokeRect(rx + lw / 2, ry + lw / 2, rw - lw, rh - lw); + } + ctx.restore(); + } + + /** Stack preview: HUD แนว cyberdeck — SCORE ซ้าย, ตาปัจจุบันขวา, เทอร์มินัลล่างซ้าย */ + function drawStackPreviewCyberHud(ctx, cw, ch) { + if (!playBotsEnabled() || !stackMini || !mapData || !isStack()) return; + const now = Date.now(); + const pad = 12; + const currentSeat = getStackPreviewCurrentSeat(); + const order = stackPreviewTurnOrder; + const mono = '11px ui-monospace, "Cascadia Mono", Consolas, monospace'; + const monoSm = '9px ui-monospace, Consolas, monospace'; + const towerMapHud = isStackTowerMissionUiMapPlay(); + const lifeSlots = towerMapHud + ? Math.max(1, Math.min(20, Math.floor(Number(playStackTeamMissesMax) || 3))) + : 3; + const livesRem = towerMapHud && stackMini.teamMissesLeft != null + ? Math.max(0, Number(stackMini.teamMissesLeft) || 0) + : Math.max(0, stackMini.lives != null ? stackMini.lives : 3); + const teamScore = stackMini.score || 0; + const combo = stackMini.combo || 0; + + function roundRectPath(x, y, w, h, r) { + const rr = Math.min(r, w / 2, h / 2); + ctx.beginPath(); + ctx.moveTo(x + rr, y); + ctx.arcTo(x + w, y, x + w, y + h, rr); + ctx.arcTo(x + w, y + h, x, y + h, rr); + ctx.arcTo(x, y + h, x, y, rr); + ctx.arcTo(x, y, x + w, y, rr); + ctx.closePath(); + } + + /** @param {{ headBust?: boolean, headSrcFrac?: number, bustLiftPx?: number }} [avatarOpts] — headBust + bustLiftPx ดันรูปขึ้นในกรอบ */ + function drawHudAvatar(cx, baselineY, maxSize, characterId, playTint, avatarOpts) { + const timeMs = now; + const dir = 'down'; + const rawImg = getAvatarImg(characterId, dir, timeMs, false); + const charImg = playTint ? getPlayTintedAvatarSource(rawImg, characterId, dir, timeMs, false, playTint) : rawImg; + let iw = 0; let ih = 0; + if (charImg && charImg.tagName === 'CANVAS' && charImg.width > 0 && charImg.height > 0) { + iw = charImg.width; + ih = charImg.height; + } else if (charImg && charImg.complete && charImg.naturalWidth) { + iw = charImg.naturalWidth; + ih = charImg.naturalHeight; + } + const headBust = !!(avatarOpts && avatarOpts.headBust); + let sx = 0; + let sy = 0; + let sww = iw; + let shh = ih; + if (headBust && iw > 0 && ih > 0) { + const rawF = Number(avatarOpts && avatarOpts.headSrcFrac); + const frac = Number.isFinite(rawF) && rawF > 0.22 && rawF < 0.85 ? rawF : 0.4; + shh = Math.max(8, Math.round(ih * frac)); + } + const ax = cx - maxSize / 2; + const ay = baselineY - maxSize; + ctx.save(); + roundRectPath(ax, ay, maxSize, maxSize, 5); + ctx.clip(); + if (iw > 0 && ih > 0) { + const maxScale = headBust ? 2.45 : 1; + const scale = Math.min(maxSize / sww, maxSize / shh, maxScale); + const dw = sww * scale; + const dh = shh * scale; + const rawLift = headBust && avatarOpts ? Number(avatarOpts.bustLiftPx) : NaN; + const bustLift = headBust + ? (Number.isFinite(rawLift) ? Math.max(0, Math.min(maxSize * 0.45, rawLift)) : 22) + : 0; + ctx.drawImage(charImg, sx, sy, sww, shh, cx - dw / 2, baselineY - dh - bustLift, dw, dh); + } else { + ctx.fillStyle = 'rgba(80, 200, 255, 0.35)'; + ctx.fillRect(ax, ay, maxSize, maxSize); + } + ctx.restore(); + ctx.strokeStyle = 'rgba(0, 234, 255, 0.5)'; + ctx.lineWidth = 1.5; + roundRectPath(ax, ay, maxSize, maxSize, 5); + ctx.stroke(); + } + + function resolveEntryVisual(entry) { + if (entry.kind === 'human') { + return { + name: String(me.nickname || 'YOU').toUpperCase(), + score: me.stackPreviewHumanPts || 0, + characterId: me.characterId, + playTint: me.playTint || playTintFromPeerId(String(myId || 'me')), + }; + } + const o = entry.botId ? others.get(entry.botId) : null; + return { + name: o ? String(o.nickname || 'BOT').replace(/\s+/g, ' ').toUpperCase().slice(0, 12) : '—', + score: o ? (o.stackBotScore || 0) : 0, + characterId: o ? o.characterId : null, + playTint: o ? (o.playTint || playTintFromPeerId(entry.botId)) : null, + }; + } + + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + + const boardW = Math.min(248, Math.max(200, cw * 0.36)); + const rowH = 42; + const headH = 38; + const boardH = headH + STACK_PREVIEW_TURN_COUNT * rowH + 10; + + if (!towerMapHud) { + ctx.fillStyle = 'rgba(8, 12, 28, 0.9)'; + roundRectPath(pad, 8, boardW, boardH, 8); + ctx.fill(); + ctx.strokeStyle = 'rgba(0, 230, 255, 0.5)'; + ctx.lineWidth = 1.5; + roundRectPath(pad, 8, boardW, boardH, 8); + ctx.stroke(); + + ctx.fillStyle = '#5cefff'; + ctx.font = 'bold 12px ui-sans-serif, system-ui, sans-serif'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + ctx.fillText('SCORE', pad + 14, 16); + ctx.fillStyle = 'rgba(94, 239, 255, 0.55)'; + ctx.font = monoSm; + ctx.fillText('shared stack · P1–P6', pad + 62, 18); + + let rowY = 8 + headH; + const drawRows = (entries) => { + entries.forEach((entry) => { + const isCurrent = entry.seat === currentSeat; + const v = resolveEntryVisual(entry); + let actionFlash = false; + if (entry.kind === 'human') { + actionFlash = lastStackPreviewActorId === '__human__' && now < lastStackPreviewActorUntil; + } else if (entry.botId) { + const ob = others.get(entry.botId); + actionFlash = !!(ob && ((now < (ob.stackBotGlowUntil || 0)) || (entry.botId === lastStackPreviewActorId && now < lastStackPreviewActorUntil))); + } + if (isCurrent) { + ctx.fillStyle = 'rgba(255, 0, 180, 0.1)'; + roundRectPath(pad + 6, rowY - 1, boardW - 12, rowH - 2, 4); + ctx.fill(); + ctx.strokeStyle = 'rgba(0, 255, 220, 0.9)'; + ctx.lineWidth = 2; + roundRectPath(pad + 6, rowY - 1, boardW - 12, rowH - 2, 4); + ctx.stroke(); + } else if (actionFlash) { + ctx.strokeStyle = 'rgba(255, 214, 102, 0.65)'; + ctx.lineWidth = 1.5; + roundRectPath(pad + 6, rowY - 1, boardW - 12, rowH - 2, 4); + ctx.stroke(); + } + const avS = 34; + drawHudAvatar(pad + 10 + avS / 2, rowY + avS - 3, avS, v.characterId, v.playTint); + ctx.fillStyle = isCurrent ? '#e0f7ff' : '#89b4fa'; + ctx.font = '600 10px ui-sans-serif, system-ui, sans-serif'; + ctx.textBaseline = 'middle'; + ctx.fillText('P' + entry.seat, pad + 14 + avS + 6, rowY + 12); + ctx.fillStyle = '#c0caf5'; + ctx.font = 'bold 10px ' + mono; + ctx.fillText(v.name.slice(0, 11), pad + 14 + avS + 6, rowY + 28); + ctx.fillStyle = actionFlash ? '#ffe066' : '#7dfcff'; + ctx.font = 'bold 13px ' + mono; + ctx.textAlign = 'right'; + ctx.fillText(String(v.score), pad + boardW - 12, rowY + rowH / 2); + ctx.textAlign = 'left'; + rowY += rowH; + }); + }; + + if (order && order.length === STACK_PREVIEW_TURN_COUNT) { + drawRows(order); + } else { + const bots = [...others.keys()].filter(isPreviewBotId).sort(); + const rows = [{ kind: 'human', seat: 1 }]; + for (let i = 0; i < STACK_PREVIEW_TURN_COUNT - 1; i++) { + rows.push({ kind: 'bot', seat: i + 2, botId: bots[i] || null }); + } + drawRows(rows); + } + } + + const framePad = 10; + const bigAv = Math.min(72, Math.max(52, ch * 0.10)); + const avBox = bigAv + 10; + const integrityMinW = 120; + const rightW = Math.min(300, Math.max(236, Math.round(cw * 0.34))); + let rightX = cw - pad - rightW; + const topY = 8; + const leftReserve = towerMapHud ? (pad + 8) : (pad + boardW + 14); + if (rightX < leftReserve) { + rightX = Math.max(pad, cw - pad - rightW); + } + const integrityW = Math.max(integrityMinW, rightW - framePad * 2 - avBox - 10); + const innerTop = topY + framePad; + const integrityX = rightX + framePad; + const integrityH = avBox + 2; + const avFrameX = integrityX + integrityW + 10; + const avFrameY = innerTop; + const footerH = towerMapHud ? 0 : 26; + const turnPanelH = framePad + integrityH + (towerMapHud ? framePad : (10 + footerH + framePad)); + + ctx.fillStyle = 'rgba(8, 12, 28, 0.9)'; + roundRectPath(rightX, topY, rightW, turnPanelH, 8); + ctx.fill(); + ctx.strokeStyle = 'rgba(0, 230, 255, 0.55)'; + ctx.lineWidth = 1.5; + roundRectPath(rightX, topY, rightW, turnPanelH, 8); + ctx.stroke(); + + let curEntry = null; + if (order && order.length === STACK_PREVIEW_TURN_COUNT) { + curEntry = order.find((e) => e.seat === currentSeat) || null; + } + const curV = curEntry ? resolveEntryVisual(curEntry) : resolveEntryVisual({ kind: 'human', seat: 1 }); + + drawStackTowerLifeIntegrityBarPlay(ctx, integrityX, innerTop - 2, integrityW, integrityH + 4, lifeSlots, livesRem); + + const avCx = avFrameX + avBox / 2; + ctx.save(); + ctx.shadowColor = 'rgba(0, 234, 255, 0.5)'; + ctx.shadowBlur = 12; + ctx.strokeStyle = 'rgba(0, 234, 255, 0.92)'; + ctx.lineWidth = 2; + roundRectPath(avFrameX - 1, avFrameY - 1, avBox + 2, avBox + 2, 6); + ctx.stroke(); + ctx.restore(); + ctx.strokeStyle = 'rgba(0, 234, 255, 0.45)'; + ctx.lineWidth = 1; + roundRectPath(avFrameX - 1, avFrameY - 1, avBox + 2, avBox + 2, 6); + ctx.stroke(); + drawHudAvatar(avCx, avFrameY + avBox - 5, bigAv, curV.characterId, curV.playTint, towerMapHud ? { headBust: true } : undefined); + + if (!towerMapHud) { + const footY = innerTop + integrityH + 12; + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + ctx.fillStyle = livesRem <= 1 ? '#f7768e' : 'rgba(125, 252, 255, 0.88)'; + ctx.font = monoSm; + ctx.fillText( + 'P' + currentSeat + ' · ' + curV.name.slice(0, 9) + ' · TEAM ' + teamScore + ' · COMBO x' + combo, + rightX + rightW / 2, + footY, + ); + } + + const termW = Math.min(380, Math.max(220, cw * 0.42)); + const logLineH = 12; + const logMaxLines = 5; + const logBlockH = logMaxLines * logLineH; + /** หัวข้อ + บล็อกล็อก + ช่องว่าง + ป้าย TOWER_LINK + แถบ — อย่าให้ทับกัน (เดิม termH=86 แคบเกิน) */ + const termH = Math.max(118, 22 + logBlockH + 10 + 14 + 8 + 10); + const termY = Math.max((towerMapHud ? 28 : boardH + 24), ch - termH - 52); + ctx.fillStyle = 'rgba(18, 8, 32, 0.88)'; + roundRectPath(pad, termY, termW, termH, 6); + ctx.fill(); + ctx.strokeStyle = 'rgba(187, 100, 255, 0.45)'; + ctx.lineWidth = 1.5; + roundRectPath(pad, termY, termW, termH, 6); + ctx.stroke(); + + ctx.fillStyle = '#c099ff'; + ctx.font = '600 10px ' + mono; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + ctx.fillText('// NODE_LOG', pad + 10, termY + 8); + ctx.fillStyle = '#7dcfff'; + ctx.font = monoSm; + const logTop = termY + 24; + let ly = logTop; + const logLines = stackPreviewHudLog.length + ? stackPreviewHudLog.slice(-logMaxLines) + : ['>>> Awaiting stack sync…', '>>> Space / click = release (your turn only)']; + logLines.forEach((ln) => { + ctx.fillText(ln, pad + 10, ly); + ly += logLineH; + }); + + const decryptPct = stackTowerDecryptPctPlay(); + const barPadX = 10; + const barH = 8; + const barW = termW - barPadX * 2; + const barY = termY + termH - barH - 10; + const labelY = barY - 14; + if (labelY >= logTop + 4) { + ctx.fillStyle = '#94e2d5'; + ctx.font = monoSm; + ctx.textBaseline = 'top'; + ctx.fillText('TOWER_LINK ' + decryptPct + '%', pad + barPadX, labelY); + } + ctx.fillStyle = 'rgba(0,0,0,0.35)'; + ctx.fillRect(pad + barPadX, barY, barW, barH); + ctx.fillStyle = 'rgba(0, 255, 200, 0.65)'; + ctx.fillRect(pad + barPadX, barY, Math.max(4, barW * decryptPct / 100), barH); + ctx.strokeStyle = 'rgba(0, 234, 255, 0.4)'; + ctx.strokeRect(pad + barPadX, barY, barW, barH); + + ctx.restore(); + } + + function useCyberPlayHud() { + return !!(mapData && (isJumpSurvive() || isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() + || isQuizQuestionMissionHudActivePlay() || isStackTowerMissionHudActivePlay())); + } + + /** Cyber HUD: quiz_carry ใช้ playLiveQuizScores (ถูก +QUIZ_CARRY_POINTS_PER_CORRECT ตอนส่งป้ายถูกที่ฮับ) */ + function cyberHudQuizCarryPeerScore(peerId) { + if (peerId == null) return 0; + return Math.max(0, Number(playLiveQuizScores[peerId]) || 0); + } + + /** แมป mng8a80o ช่วงเล่น: คำถามอยู่บนแผนที่ (โซนทอง) — HUD กลางเหลือ TIME + แผ่นเวลา */ + function syncQuizQuestionMissionCyberCenterHud() { + const root = document.getElementById('play-cyber-hud'); + const qBand = document.getElementById('play-cyber-quiz-mission-q-band'); + const qText = document.getElementById('play-cyber-quiz-mission-q-text'); + const qPlaque = document.getElementById('play-cyber-quiz-mission-q-plaque'); + const tb = root ? root.querySelector('.play-cyber-time-block') : null; + const on = isQuizQuestionMissionHudActivePlay() || isStackTowerMissionHudActivePlay() || isJumpSurviveMissionHudActivePlay() + || isSpaceShooterMissionHudActivePlay(); + if (isQuizQuestionMissionUiMapPlay()) { + if (qBand) { + qBand.classList.add('is-hidden'); + qBand.setAttribute('aria-hidden', 'true'); + } + if (qText) qText.textContent = ''; + if (qPlaque) { + qPlaque.classList.add('is-hidden'); + qPlaque.removeAttribute('src'); + delete qPlaque.dataset.qmQsrc; + } + } + const plaqueImg = root ? document.getElementById('play-cyber-time-plaque-img') : null; + const head = tb && tb.querySelector ? tb.querySelector('.play-cyber-time-head') : null; + const clearQmTimePlaque = () => { + if (plaqueImg) { + plaqueImg.onload = null; + plaqueImg.onerror = null; + plaqueImg.removeAttribute('src'); + plaqueImg.classList.add('is-hidden'); + plaqueImg.setAttribute('aria-hidden', 'true'); + delete plaqueImg.dataset.qmTimeSrc; + } + if (head) head.classList.remove('play-cyber-time-head--qm-plaque'); + if (tb) { + tb.style.backgroundImage = ''; + tb.style.backgroundSize = ''; + tb.style.backgroundPosition = ''; + tb.style.backgroundRepeat = ''; + tb.style.paddingTop = ''; + tb.style.minWidth = ''; + delete tb.dataset.qmTimeSrc; + } + }; + if (tb && plaqueImg && head) { + const tUrl = questionMissionHudAssetUrl('time.png'); + if (on) { + tb.style.backgroundImage = ''; + tb.style.backgroundSize = ''; + tb.style.backgroundPosition = ''; + tb.style.backgroundRepeat = ''; + tb.style.paddingTop = ''; + tb.style.minWidth = ''; + const applyPlaque = () => { + head.classList.add('play-cyber-time-head--qm-plaque'); + plaqueImg.classList.remove('is-hidden'); + plaqueImg.setAttribute('aria-hidden', 'false'); + }; + if (plaqueImg.dataset.qmTimeSrc === tUrl && plaqueImg.getAttribute('src') === tUrl + && plaqueImg.complete && (plaqueImg.naturalWidth || 0) > 0) { + applyPlaque(); + } else if (plaqueImg.dataset.qmTimeSrc !== tUrl || plaqueImg.getAttribute('src') !== tUrl) { + plaqueImg.dataset.qmTimeSrc = tUrl; + plaqueImg.onload = function () { + if ((plaqueImg.naturalWidth || 0) > 0) applyPlaque(); + else clearQmTimePlaque(); + }; + plaqueImg.onerror = function () { + clearQmTimePlaque(); + }; + plaqueImg.setAttribute('src', tUrl); + } + } else { + clearQmTimePlaque(); + } + } + } + + function playCyberHudEsc(t) { + return String(t == null ? '' : t) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + /** แถบ Cyber SCORE — ใช้ tint เดียวกับในเกม (ไม่ใช้ PNG รวมซิลูเอตขาวจาก getCharacterImg อย่างเดียว) */ + function setCyberHudScoreAvatarImg(avImg, row) { + const cid = row.characterId ? String(row.characterId) : ''; + if (!cid) { + avImg.src = defaultAvatarImg.src; + return; + } + const dir = 'down'; + const nowT = Date.now(); + const walk = false; + const rawImg = getAvatarImg(cid, dir, nowT, walk); + const peerId = row.id; + const tint = row.isMe + ? (me.playTint || playTintFromPeerId(String(myId != null ? myId : 'me'))) + : (() => { + const ox = others.get(peerId); + return ox ? (ox.playTint || playTintFromPeerId(String(peerId))) : playTintFromPeerId(String(peerId)); + })(); + const cacheKey = [row.isMe ? 'me' : String(peerId), cid, tint.head, tint.hair, tint.body].join('|'); + const comp = rawImg && cid && tint + ? getPlayTintedAvatarSource(rawImg, cid, dir, nowT, walk, tint) + : rawImg; + if (comp && comp.tagName === 'CANVAS' && comp.width > 0) { + let dataUrl = cyberHudScoreAvatarUrlCache.get(cacheKey); + if (!dataUrl) { + try { + dataUrl = comp.toDataURL('image/png'); + } catch (e) { + dataUrl = ''; + } + if (dataUrl) { + if (cyberHudScoreAvatarUrlCache.size >= CYBER_HUD_AV_URL_CACHE_CAP) { + const first = cyberHudScoreAvatarUrlCache.keys().next(); + if (!first.done) cyberHudScoreAvatarUrlCache.delete(first.value); + } + cyberHudScoreAvatarUrlCache.set(cacheKey, dataUrl); + } + } + if (dataUrl) { + avImg.src = dataUrl; + return; + } + const im = getCharacterImg(cid, dir); + avImg.src = im && im.src ? im.src : defaultAvatarImg.src; + return; + } + if (comp && comp.src) { + avImg.src = comp.src; + } else { + const im = getCharacterImg(cid, dir); + avImg.src = im && im.src ? im.src : defaultAvatarImg.src; + } + } + + function quizQuestionMissionJoystickResetVisual() { + quizQuestionMissionJoyVecX = 0; + quizQuestionMissionJoyVecY = 0; + const knob = document.getElementById('quiz-q-mission-joystick-knob'); + if (knob) knob.style.transform = 'translate(-50%, -50%)'; + } + + function quizQuestionMissionJoystickUpdateFromClientXY(clientX, clientY) { + const base = document.getElementById('quiz-q-mission-joystick-base'); + const knob = document.getElementById('quiz-q-mission-joystick-knob'); + if (!base || !knob) return; + const r = base.getBoundingClientRect(); + const cx = r.left + r.width * 0.5; + const cy = r.top + r.height * 0.5; + let dx = clientX - cx; + let dy = clientY - cy; + const half = Math.min(r.width, r.height) * 0.5; + const maxKn = Math.max(18, half * 0.52); + const len = Math.hypot(dx, dy); + if (len > maxKn && len > 1e-6) { + dx = (dx / len) * maxKn; + dy = (dy / len) * maxKn; + } + const nlen = Math.hypot(dx, dy); + const dead = maxKn * 0.12; + if (nlen < dead) { + quizQuestionMissionJoyVecX = 0; + quizQuestionMissionJoyVecY = 0; + knob.style.transform = 'translate(-50%, -50%)'; + return; + } + quizQuestionMissionJoyVecX = dx / maxKn; + quizQuestionMissionJoyVecY = dy / maxKn; + knob.style.transform = 'translate(calc(-50% + ' + dx + 'px), calc(-50% + ' + dy + 'px))'; + } + + function syncQuizQuestionMissionJoystickPlay() { + const root = document.getElementById('quiz-question-mission-joystick'); + if (!root) return; + const on = !!(isQuizQuestionMissionHudActivePlay() && !isChatFocused()); + root.classList.toggle('is-hidden', !on); + root.setAttribute('aria-hidden', on ? 'false' : 'true'); + if (!on) { + const base = document.getElementById('quiz-q-mission-joystick-base'); + if (base && quizQuestionMissionJoyPointerId != null) { + try { + base.releasePointerCapture(quizQuestionMissionJoyPointerId); + } catch (_e) { /* ignore */ } + } + quizQuestionMissionJoyPointerId = null; + quizQuestionMissionJoystickResetVisual(); + } + } + + function syncPlayCyberHud() { + const root = document.getElementById('play-cyber-hud'); + const stackEl = document.getElementById('play-canvas-stack'); + const on = useCyberPlayHud(); + if (root) { + root.classList.toggle('is-hidden', !on); + root.setAttribute('aria-hidden', on ? 'false' : 'true'); + if (!on) { + root.classList.remove('play-cyber-hud--question-mission'); + root.classList.remove('play-cyber-hud--score-flush-left'); + root.classList.remove('play-cyber-hud--jump-mission-hide-score'); + root.classList.remove('play-cyber-hud--ss-self-integrity-left'); + root.classList.remove('play-cyber-hud--last-light-no-time'); + } + } + if (stackEl) stackEl.classList.toggle('play-cyber-vignette', !!on); + const gw = document.querySelector('.game-wrap'); + if (gw) gw.classList.toggle('play-cyber-active', !!on); + if (!on || !root) return; + root.classList.toggle( + 'play-cyber-hud--question-mission', + !!(isQuizQuestionMissionHudActivePlay() || isStackTowerMissionHudActivePlay() || isJumpSurviveMissionHudActivePlay() + || isSpaceShooterMissionHudActivePlay()), + ); + root.classList.toggle( + 'play-cyber-hud--stack-tower-canvas-hud', + !!(playBotsEnabled() && stackMini && isStack() && isStackTowerMissionUiMapPlay()), + ); + root.classList.toggle('play-cyber-hud--ss-self-integrity-left', !!isSpaceShooterMissionHudActivePlay()); + /** แถบ SCORE แนวนอน (mno9kb07) — ใช้รูปแบบเดียวกับ quiz_carry / พรีวิว editor */ + root.classList.toggle( + 'play-cyber-hud--gauntlet-crown-strip', + !!(on && (isGauntletCrownHeistMapPlay() || isQuizCarry())), + ); + root.classList.toggle('play-cyber-hud--score-flush-left', !!(on && isQuizCarry())); + root.classList.toggle('play-cyber-hud--jump-mission-hide-score', !!isJumpSurviveMissionUiMapPlay()); + /** Last Light mno9kb07: ไม่แสดง TIME / ซูมใต้เวลา — จบด้วยรันเวย์ ไม่ใช้นาฬิกา */ + root.classList.toggle('play-cyber-hud--last-light-no-time', !!(on && isGauntletCrownHeistMapPlay())); + const timeLabelEl = root.querySelector('.play-cyber-time-label'); + if (timeLabelEl) timeLabelEl.style.display = isQuizCarry() ? 'none' : ''; + const timeVal = document.getElementById('play-cyber-time-val'); + const timeSub = document.getElementById('play-cyber-time-sub'); + const portrait = document.getElementById('play-cyber-portrait-img'); + const statusEl = document.getElementById('play-cyber-self-status'); + const hintEl = document.getElementById('play-cyber-hint'); + const previewEl = document.getElementById('play-cyber-preview-line'); + + if (timeSub) { + const bbMegaLiveHud = isBalloonBoss() && isMegaVirusMissionShellMapPlay() && !isGauntletCrownPregameBlockingPlay(); + timeSub.textContent = isQuizQuestionMissionHudActivePlay() + ? (playQuizPhaseLocal === 'read' ? 'READ · อ่านคำถาม' + : (playQuizPhaseLocal === 'answer' ? 'ANSWER · เดินไปโซน จริง / เท็จ' + : 'QUIZ MISSION · ประลองความรู้')) + : (isSpaceShooterMissionHudActivePlay() + ? 'SPACE SHOOTER · ARCADE — TIME (sec) · ภารกิจยิงยาน' + : (isStackTowerMissionHudActivePlay() + ? 'TOWER STACK · DECRYPT — TIME (sec) · เหลือเวลาถอดรหัส' + : (isQuizCarry() + ? 'QUIZ CARRY · COURT — หยิบป้ายถูกแล้วส่งที่ฮับ (F / Grab)' + : (isGauntlet() ? 'GAUNTLET · SURVIVAL RUN' + : (isSpaceShooter() ? 'SPACE SHOOTER · ARCADE' + : (isBalloonBoss() ? (bbMegaLiveHud ? '' : 'BALLOON BOSS · MEGA VIRUS') + : (isJumpSurviveMissionUiMapPlay() ? 'JUMPER · SURVIVE — เหลือเวลา (วินาที) · TIME (sec)' : 'JUMP SURVIVE · NODE UPLINK'))))))); + timeSub.style.display = bbMegaLiveHud ? 'none' : ''; + } + const zoomHintEl = document.getElementById('play-cyber-embed-zoom-hint'); + if (zoomHintEl) { + const showEmbedZoom = !!(previewMode && editorEmbedReturn && mapData && !isQuizQuestionMissionHudActivePlay() + && !(isBalloonBoss() && isMegaVirusMissionShellMapPlay())); + if (showEmbedZoom) { + const zNum = Number(playEmbedUserZoomMul.toFixed(2)); + zoomHintEl.textContent = '×' + String(zNum); + zoomHintEl.classList.remove('is-hidden'); + zoomHintEl.setAttribute('aria-hidden', 'false'); + } else { + zoomHintEl.textContent = ''; + zoomHintEl.classList.add('is-hidden'); + zoomHintEl.setAttribute('aria-hidden', 'true'); + } + } + if (timeVal) { + if (isQuiz() && isQuizQuestionMissionUiMapPlay()) { + if (quizQuestionMissionPhase !== 'live' || quizQuestionMissionPhase === 'ended') { + timeVal.textContent = '···'; + } else if (!playQuizPhaseEndsAt) { + timeVal.textContent = '—'; + } else { + timeVal.textContent = String(Math.max(0, Math.ceil((playQuizPhaseEndsAt - Date.now()) / 1000))); + } + } else if (isStack() && isStackTowerMissionUiMapPlay()) { + if (stackTowerMissionPhase === 'live') { + const remSt = Math.max(0, Math.ceil(stackTowerMissionTimeLimitSecPlay() - stackTowerMissionElapsedSecPlay())); + timeVal.textContent = String(remSt); + } else if (stackTowerMissionPhase === 'howto' || stackTowerMissionPhase === 'countdown') { + timeVal.textContent = '···'; + } else { + timeVal.textContent = '0'; + } + } else if (isQuizCarry()) { + if (quizCarrySessionEnded) { + timeVal.textContent = '0'; + } else if (previewMode && editorEmbedReturn && quizCarryEmbedCountdownEndAt > Date.now()) { + const rem = Math.max(0, Math.ceil((quizCarryEmbedCountdownEndAt - Date.now()) / 1000)); + timeVal.textContent = String(rem); + } else if (previewMode && editorEmbedReturn && quizCarryEmbedPreOptionCountdownEndAt > Date.now()) { + const rem2 = Math.max(0, Math.ceil((quizCarryEmbedPreOptionCountdownEndAt - Date.now()) / 1000)); + timeVal.textContent = String(rem2); + } else if (quizCarryPregameActive) { + timeVal.textContent = '···'; + } else if (!quizCarryCurrent) { + timeVal.textContent = '—'; + } else if (quizCarryOptionRevealAt > 0 && Date.now() < quizCarryOptionRevealAt) { + timeVal.textContent = String(Math.max(0, Math.ceil((quizCarryOptionRevealAt - Date.now()) / 1000))); + } else if (quizCarryAnswerCloseAt > Date.now()) { + timeVal.textContent = String(Math.max(0, Math.ceil((quizCarryAnswerCloseAt - Date.now()) / 1000))); + } else { + timeVal.textContent = '0'; + } + } else if (isGauntlet()) { + if (isGauntletCrownHeistMapPlay() && isGauntletCrownPregameBlockingPlay()) { + timeVal.textContent = '···'; + } else if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) { + const rem = Math.max(0, Math.ceil((gauntletEndsAtMs - Date.now()) / 1000)); + const mm = Math.floor(rem / 60); + const ss = rem % 60; + timeVal.textContent = `${mm}:${String(ss).padStart(2, '0')}`; + } else if (gauntletRuntimeTimeLimitSec > 0) { + timeVal.textContent = '···'; + } else { + timeVal.textContent = '∞'; + } + } else if (isSpaceShooter()) { + if (isSpaceShooterMissionUiMapPlay()) { + if (spaceShooterMissionPhase !== 'live' || spaceShooterGameEnded) { + const limSs = spaceShooterTimeLimitSecPlay(); + if (spaceShooterMissionPhase === 'howto' || spaceShooterMissionPhase === 'countdown') { + timeVal.textContent = String(limSs > 0 ? limSs : 0); + } else { + timeVal.textContent = '0'; + } + } else { + const remSs = spaceShooterRemainingSecPlay(); + timeVal.textContent = remSs != null ? String(remSs) : '∞'; + } + } else if (spaceShooterGameEnded) { + timeVal.textContent = '0'; + } else { + const rem = spaceShooterRemainingSecPlay(); + timeVal.textContent = rem != null ? String(rem) : '∞'; + } + } else if (isBalloonBoss()) { + if (isMegaVirusMissionShellMapPlay() && isGauntletCrownPregameBlockingPlay()) { + timeVal.textContent = '···'; + } else if (balloonBossGameEnded) { + timeVal.textContent = '0'; + } else { + const rem = balloonBossRemainingSecPlay(); + timeVal.textContent = rem != null ? String(rem) : '∞'; + } + } else if (isJumpSurviveMissionUiMapPlay()) { + if (jumpSurviveMissionPhase !== 'live' || jumpSurviveGameEnded) { + timeVal.textContent = jumpSurviveMissionPhase === 'howto' ? '···' : (jumpSurviveMissionPhase === 'countdown' ? '···' : '0'); + } else { + const rem = jumpSurviveRemainingSecMission(); + timeVal.textContent = rem != null ? String(rem) : '∞'; + } + } else { + const t = jumpSurviveSessionStartMs > 0 + ? Math.max(0, Math.floor((performance.now() - jumpSurviveSessionStartMs) / 1000)) + : 0; + timeVal.textContent = String(t); + } + } + + if (portrait) { + const cid = me.characterId || getPlayCharacterId(); + const dir = isSpaceShooter() + ? 'down' + : (isGauntletFaceRightMapMno9kb07() ? 'right' : (me.direction || 'down')); + const nowT = Date.now(); + const walk = !!me.isWalking; + const rawImg = getAvatarImg(cid, dir, nowT, walk); + const tint = me.playTint || playTintFromPeerId(String(myId != null ? myId : 'me')); + const comp = rawImg && cid && tint + ? getPlayTintedAvatarSource(rawImg, cid, dir, nowT, walk, tint) + : rawImg; + if (comp && comp.tagName === 'CANVAS' && comp.width > 0) { + try { + portrait.src = comp.toDataURL('image/png'); + } catch (e) { + if (rawImg && rawImg.src) portrait.src = rawImg.src; + } + } else if (comp && comp.src) { + portrait.src = comp.src; + } else if (rawImg && rawImg.src) { + portrait.src = rawImg.src; + } + } + if (statusEl) { + if (isSpaceShooterMissionHudActivePlay()) { + ensureStackTowerLifeHudImagesPlay(); + const nn = String((me.nickname || nick || 'PILOT')).trim().slice(0, 24); + const hits = Math.max(0, Math.min(SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS, Number(me.spaceShooterHits) || 0)); + const rem = SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS - hits; + const esc = playCyberHudEsc(nn); + const W = 220; + const H = 58; + const dpr = Math.min(2, Math.max(1, typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1)); + let canv = statusEl.querySelector('canvas.play-cyber-ss-integrity-canvas'); + let metaEl = statusEl.querySelector('.play-cyber-ss-meta'); + if (!canv || !metaEl) { + statusEl.textContent = ''; + canv = document.createElement('canvas'); + canv.className = 'play-cyber-ss-integrity-canvas'; + canv.setAttribute('aria-hidden', 'true'); + metaEl = document.createElement('div'); + metaEl.className = 'play-cyber-ss-meta'; + statusEl.appendChild(canv); + statusEl.appendChild(metaEl); + } + canv.width = Math.max(1, Math.floor(W * dpr)); + canv.height = Math.max(1, Math.floor(H * dpr)); + canv.style.width = W + 'px'; + canv.style.height = H + 'px'; + const ctxSs = canv.getContext('2d'); + if (ctxSs) { + ctxSs.setTransform(dpr, 0, 0, dpr, 0, 0); + ctxSs.clearRect(0, 0, W + 2, H + 2); + drawStackTowerLifeIntegrityBarPlay( + ctxSs, + 2, + 2, + W - 4, + H - 4, + SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS, + rem, + ); + } + metaEl.textContent = 'P1 · ' + esc + ' · TEAM 0 · COMBO x0'; + } else if (isQuizQuestionMissionHudActivePlay()) { + statusEl.textContent = 'QUIZ LINK · ประลองความรู้'; + } else if (isStackTowerMissionHudActivePlay()) { + statusEl.textContent = 'DECRYPT UPLINK · TOWER'; + } else if (isJumpSurviveMissionHudActivePlay()) { + statusEl.textContent = 'JUMPER LINK · SURVIVE'; + } else if (isQuizCarry() && quizCarrySessionEnded) { + statusEl.textContent = 'SESSION COMPLETE · จบชุดคำถาม'; + } else if (isJumpSurvive() && jumpSurviveEliminated) { + statusEl.textContent = 'SPECTATOR · LINK SEVERED'; + } else if (isBalloonBoss() && me.balloonBossEliminated) { + statusEl.textContent = 'SPECTATOR · BALLOONS GONE'; + } else if (isGauntletCrownHeistMapPlay()) { + statusEl.textContent = ''; + } else { + statusEl.textContent = 'OPERATOR ONLINE'; + } + } + if (hintEl) { + if (isQuizQuestionMissionHudActivePlay()) { + hintEl.textContent = ''; + hintEl.setAttribute('aria-hidden', 'true'); + } else { + hintEl.removeAttribute('aria-hidden'); + if (isStackTowerMissionHudActivePlay()) { + hintEl.textContent = 'กด SPACE / ENTER หรือคลิกที่จอ = DROP · วางต่อเนื่องแม่น = โบนัส · ถอดรหัสให้ครบ 100% ภายในเวลา'; + } else if (isQuizCarry()) { + hintEl.textContent = quizCarrySessionEnded + ? 'ชุดคำถามจบแล้ว · Session ended — ดูคะแนนด้าน SCORE' + : ('ยืนโซนตัวเลือกแล้วกด F/Grab หยิบ · ถือป้ายถูกแล้วไปฮับส่ง — ถูก +' + QUIZ_CARRY_POINTS_PER_CORRECT + ' แต้ม (correct +' + QUIZ_CARRY_POINTS_PER_CORRECT + ' on hub)'); + } else if (isJumpSurvive()) { + if (isJumpSurviveMissionUiMapPlay()) { + hintEl.textContent = jumpSurviveEliminated + ? 'คุณตกน้ำแล้ว — ดูเพื่อนต่อ — รอจบเพื่อรับคะแนน (ผู้รอดได้ 100 คะแนน)' + : 'กระโดดตามแท่น — หลีกหนาม/กับดักด้านล่าง · หมดเวลาแล้วสรุปคะแนน (ผู้รอด 100) · Space / W / ↑ กระโดด · A D / ← → เดิน'; + } else { + hintEl.textContent = jumpSurviveEliminated + ? 'Spectate remaining nodes · Exit room to reset session' + : 'Space / W / ↑ jump · A D / arrows move · Hit ceiling/floor = out (watch others)'; + } + } else if (isSpaceShooter()) { + hintEl.textContent = spaceShooterGameEnded + ? 'จบแล้ว · Session ended — ดูอันดับบน overlay / see rankings on overlay' + : 'A D / ← → = move · W S / ↑ ↓ = up/down (lower half of map only) · Space = fire · +5 per hit'; + } else if (isBalloonBoss()) { + hintEl.textContent = balloonBossGameEnded + ? 'จบแล้ว · Session ended — see overlay' + : (me.balloonBossEliminated + ? 'คุณตกรอบแล้ว — ดูเพื่อนเล่น · You are out — spectate' + : (isMegaVirusMissionShellMapPlay() + ? 'ลูกศรบนวงหมุนเอง — ยิงตามมุมลูกศร ณ ตอนกด Space · Ring spins · Space = fire along arrow' + : 'ยานมีแรงเฉื่อย — A D / arrows / W S เร่งทิศทาง · ปล่อยปุ่มแล้วยังไหล (แรงหน่วง) · Space = ยิง (ดีเลย์) · ลูกโป้งหมด = ตกรอบ')); + } else if (isGauntletCrownHeistMapPlay()) { + hintEl.textContent = 'Space / W / ↑ jump · Start 100 pts · Hit -10 · Fall off left edge = out · Rank bonus at end'; + } else { + hintEl.textContent = 'Space / W / ↑ jump · Lanes + lasers · Clear = right + score'; + } + } + } + if (previewEl) { + if (playBotsEnabled() && mapData) { + const human = countPlayHumans(); + const bots = [...others.keys()].filter(isPreviewBotId).length; + const botTarget = playBotTargetHeadcount(); + const simLabel = detectiveCaseFillBots ? 'CASE' : 'SIM'; + previewEl.textContent = `${simLabel} · humans ${human} + bots ${bots} · target ${botTarget}`; + previewEl.classList.remove('is-hidden'); + } else { + previewEl.textContent = ''; + previewEl.classList.add('is-hidden'); + } + } + + syncQuizQuestionMissionCyberCenterHud(); + + const ul = document.getElementById('play-cyber-score-list'); + if (!ul) return; + /* mnptfts2 — ไม่แสดงแผง SCORE (เหลือ TIME + โปรไฟล์) */ + if (isJumpSurviveMissionUiMapPlay()) { + ul.innerHTML = ''; + const boardSkip = ul.closest('.play-cyber-scoreboard'); + if (boardSkip) boardSkip.classList.remove('play-cyber-scoreboard--crown-strip'); + ul.classList.remove('play-cyber-score-list--crown-strip'); + return; + } + const stripScoreHud = isGauntletCrownHeistMapPlay() || isQuizCarry(); + const board = ul.closest('.play-cyber-scoreboard'); + if (board) board.classList.toggle('play-cyber-scoreboard--crown-strip', !!stripScoreHud); + ul.classList.toggle('play-cyber-score-list--crown-strip', !!stripScoreHud); + const panelTitle = root.querySelector('.play-cyber-panel-title'); + const crownHead = document.getElementById('play-cyber-crown-score-head'); + const crownImg = document.getElementById('play-cyber-crown-score-img'); + if (panelTitle && crownHead && crownImg) { + if (stripScoreHud) { + crownHead.classList.add('is-hidden'); + crownHead.setAttribute('aria-hidden', 'true'); + crownImg.removeAttribute('src'); + delete crownImg.dataset.gauntletScoreSrc; + crownImg.alt = ''; + crownImg.onerror = null; + crownImg.onload = null; + panelTitle.textContent = 'SCORE :'; + panelTitle.classList.remove('is-hidden'); + } else { + crownHead.classList.add('is-hidden'); + crownHead.setAttribute('aria-hidden', 'true'); + crownImg.removeAttribute('src'); + delete crownImg.dataset.gauntletScoreSrc; + crownImg.alt = ''; + crownImg.onerror = null; + crownImg.onload = null; + panelTitle.textContent = 'SCORE'; + panelTitle.classList.remove('is-hidden'); + } + } + const safeYv = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1); + const jumpMissionHud = isJumpSurvive() && isJumpSurviveMissionUiMapPlay(); + const quizMissionHud = isQuizQuestionMissionHudActivePlay(); + const stackMissionHud = isStackTowerMissionHudActivePlay(); + const jumpMissionLiveHud = isJumpSurviveMissionHudActivePlay(); + const spaceMissionLiveHud = isSpaceShooterMissionHudActivePlay(); + /** แถบ SCORE แบบ mock mng8a80o (แนวนอน อวาตาร์+ชื่อ | คะแนน) — ภารกิจคำถาม + Stack Tower + Jumper mnptfts2 + Space mnpz6rkp */ + const cyberQmMockHud = quizMissionHud || stackMissionHud || jumpMissionLiveHud || spaceMissionLiveHud; + const stackTeamPts = Math.max(0, Number(stackMini && stackMini.score) || 0); + const rows = []; + rows.push({ + id: myId, + nickname: me.nickname || nick, + characterId: me.characterId || getPlayCharacterId(), + score: (isQuizCarry() || quizMissionHud) ? cyberHudQuizCarryPeerScore(myId) + : (stackMissionHud + ? (playBotsEnabled() ? (me.stackPreviewHumanPts || 0) : stackTeamPts) + : (isGauntlet() ? (me.gauntletScore || 0) + : (isSpaceShooter() ? (me.spaceShooterScore || 0) + : (isBalloonBoss() ? (me.balloonBossScore || 0) + : (jumpMissionHud ? (jumpSurviveEliminated ? 0 : 100) : 0))))), + y: safeYv(me.y), + eliminated: !!(isJumpSurvive() && jumpSurviveEliminated) || !!(isBalloonBoss() && me.balloonBossEliminated) + || !!(isGauntletCrownHeistMapPlay() && me.gauntletEliminated), + isMe: true, + }); + others.forEach((o, id) => { + rows.push({ + id, + nickname: o.nickname || id.slice(0, 8), + characterId: o.characterId, + score: (isQuizCarry() || quizMissionHud) ? cyberHudQuizCarryPeerScore(id) + : (stackMissionHud + ? (playBotsEnabled() && isPreviewBotId(id) ? (o.stackBotScore || 0) : stackTeamPts) + : (isGauntlet() ? (o.gauntletScore || 0) + : (isSpaceShooter() ? (o.spaceShooterScore || 0) + : (isBalloonBoss() ? (o.balloonBossScore || 0) + : (jumpMissionHud ? (o.jumpSurviveEliminated ? 0 : 100) : 0))))), + y: safeYv(o.y), + eliminated: !!(isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated) + || !!(isBalloonBoss() && o.balloonBossEliminated) + || !!(isGauntletCrownHeistMapPlay() && o.gauntletEliminated), + isMe: false, + }); + }); + let leaderId = null; + if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud || quizMissionHud || stackMissionHud) { + let mx = -1; + for (let i = 0; i < rows.length; i++) { + if (rows[i].score > mx) { + mx = rows[i].score; + leaderId = rows[i].id; + } + } + } else { + let best = Infinity; + for (let i = 0; i < rows.length; i++) { + if (!rows[i].eliminated && rows[i].y < best) { + best = rows[i].y; + leaderId = rows[i].id; + } + } + } + rows.sort((a, b) => { + if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud || quizMissionHud || stackMissionHud) { + if (b.score !== a.score) return b.score - a.score; + if (a.eliminated !== b.eliminated) return a.eliminated ? 1 : -1; + return String(a.nickname || '').localeCompare(String(b.nickname || ''), 'th'); + } + if (a.eliminated !== b.eliminated) return a.eliminated ? 1 : -1; + return a.y - b.y; + }); + + ul.innerHTML = ''; + const cap = stripScoreHud ? (isQuizCarry() ? 8 : 6) : 8; + let hudRows = rows.slice(0, Math.min(cap, rows.length)); + if (stripScoreHud) { + const mi = hudRows.findIndex((row) => row.isMe); + if (mi >= 0) { + const mine = hudRows.splice(mi, 1)[0]; + hudRows.push(mine); + } + } + for (let r = 0; r < hudRows.length; r++) { + const row = hudRows[r]; + const li = document.createElement('li'); + li.className = 'play-cyber-score-row' + (row.isMe ? ' is-me' : '') + (row.eliminated ? ' is-out' : '') + + (cyberQmMockHud ? ' play-cyber-score-row--qm-mock' : '') + + (stripScoreHud ? ' play-cyber-score-row--crown-strip' : ''); + const av = document.createElement('img'); + av.className = 'play-cyber-score-av' + (cyberQmMockHud ? ' play-cyber-score-av--qm' : '') + + (stripScoreHud ? ' play-cyber-score-av--crown-strip' : ''); + av.alt = ''; + setCyberHudScoreAvatarImg(av, row); + const sc = document.createElement('span'); + sc.className = 'play-cyber-score-val' + (cyberQmMockHud ? ' play-cyber-qm-score' : '') + + (stripScoreHud ? ' play-cyber-crown-strip-val' : ''); + if (isGauntlet() || isSpaceShooter() || isBalloonBoss() || isQuizCarry() || jumpMissionHud || quizMissionHud || stackMissionHud) { + sc.textContent = String(row.score); + } else { + sc.textContent = row.eliminated ? '—' : String(r + 1); + } + if (cyberQmMockHud) { + if (row.id === leaderId && leaderId != null) { + const crown = document.createElement('span'); + crown.className = 'play-cyber-lead-crown'; + crown.textContent = '♔'; + crown.title = 'Leader'; + sc.insertBefore(crown, sc.firstChild); + } + const qmLeft = document.createElement('div'); + qmLeft.className = 'play-cyber-qm-left'; + const avWrap = document.createElement('span'); + avWrap.className = 'play-cyber-qm-av-wrap'; + avWrap.appendChild(av); + const qmName = document.createElement('span'); + qmName.className = 'play-cyber-qm-name'; + qmName.textContent = String(row.nickname || '').trim().slice(0, 28); + qmLeft.appendChild(avWrap); + qmLeft.appendChild(qmName); + li.appendChild(qmLeft); + li.appendChild(sc); + } else if (stripScoreHud) { + const cell = document.createElement('div'); + cell.className = 'play-cyber-crown-strip-cell'; + const avWrap = document.createElement('div'); + avWrap.className = 'play-cyber-crown-strip-av-wrap'; + avWrap.appendChild(av); + const meta = document.createElement('div'); + meta.className = 'play-cyber-crown-strip-meta'; + const name = document.createElement('span'); + name.className = 'play-cyber-crown-strip-name'; + if (row.id === leaderId && leaderId != null) { + const crown = document.createElement('span'); + crown.className = 'play-cyber-lead-crown'; + crown.textContent = '♔'; + crown.title = 'Leader'; + name.appendChild(crown); + } + name.appendChild(document.createTextNode(String(row.nickname || '').trim().slice(0, 16))); + meta.appendChild(name); + meta.appendChild(sc); + cell.appendChild(avWrap); + cell.appendChild(meta); + li.appendChild(cell); + } else { + const mid = document.createElement('div'); + mid.className = 'play-cyber-score-mid'; + const name = document.createElement('span'); + name.className = 'play-cyber-score-name'; + if (row.id === leaderId && leaderId != null) { + const crown = document.createElement('span'); + crown.className = 'play-cyber-lead-crown'; + crown.textContent = '♔'; + crown.title = 'Leader'; + name.appendChild(crown); + } + name.appendChild(document.createTextNode(String(row.nickname || '').slice(0, 14))); + const st = document.createElement('span'); + st.className = 'play-cyber-score-state'; + st.textContent = (isQuizCarry() || quizMissionHud) + ? '' + : (row.eliminated + ? (isBalloonBoss() ? 'OUT' : (isGauntletCrownHeistMapPlay() ? 'เสียชีวิต' : 'OFFLINE')) + : (isBalloonBoss() + ? ('♥' + Math.max(0, row.isMe ? (me.balloonBossBalloons | 0) : ((() => { + const ox = others.get(row.id); + return ox ? (ox.balloonBossBalloons | 0) : 0; + })()))) + : 'LINK_OK')); + mid.appendChild(name); + mid.appendChild(st); + li.appendChild(av); + li.appendChild(mid); + li.appendChild(sc); + } + ul.appendChild(li); + } + } + + function draw() { + try { + if (!mapData) { + document.documentElement.classList.remove('play-stack-tower-pixel-canvas'); + return; + } + document.documentElement.classList.toggle('play-stack-tower-pixel-canvas', isStackTowerMissionUiMapPlay()); + } catch (e) { /* ignore */ } + if (isGauntletCrownHeistMapPlay()) ensureGauntletCrownScorePenaltyImgPlay(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + const w = mapData.width, h = mapData.height; + + let zDraw = computePlayCameraZDrawPlay(); + const stackCam = isStack() ? getStackCameraCentersPx() : null; + let camX = stackCam ? stackCam.px : me.x * tileSize; + let camY = stackCam ? stackCam.py : me.y * tileSize; + if (isJumpSurvive()) { + camX = jumpSurviveCamCenterX; + camY = jumpSurviveCamCenterY; + } + if (isSpaceShooter() || isBalloonBoss()) { + const mwPx = w * tileSize, mhPx = h * tileSize; + camX = mwPx * 0.5; + camY = mhPx * 0.5; + } else if (isQuizCarry()) { + const qc = getQuizCarryMapCameraWorldCenterPxPlay(); + if (qc) { + camX = qc.cx; + camY = qc.cy; + } + } else if (isQuizQuestionMissionHudActivePlay()) { + const qmc = getQuizQuestionMissionMapCenterWorldPxPlay(); + if (qmc) { + camX = qmc.cx; + camY = qmc.cy; + } + } + const gauntletGroupCam = getGauntletCrownHeistGroupCameraCenterPxPlay(tileSize, canvas.width, canvas.height, zDraw); + if (gauntletGroupCam) { + camX = gauntletGroupCam.px; + camY = gauntletGroupCam.py; + } + /* Stack Tower (mnn93hpi): ไม่เลื่อนกล้องตาม floorWorldY — ใช้ cam จาก getStackCameraCentersPx() เหมือน Stack ทั่วไป (วางบล็อกแล้วจอไม่ไหล) */ + lastPlayZDrawForInput = zDraw; + const stackTowerWorldScrollScreenY = getStackTowerWorldLayerScrollScreenOffsetYPlay(zDraw); + const halfW = canvas.width / (2 * zDraw); + const halfH = canvas.height / (2 * zDraw); + + // world bounds ที่กล้องมองเห็น (เป็นพิกัดพิกเซลของ map) + const worldMinX = camX - halfW; + const worldMaxX = camX + halfW; + const worldMinY = camY - halfH; + const worldMaxY = camY + halfH; + + const mapWpx = w * tileSize, mapHpx = h * tileSize; + const visibleW = worldMaxX - worldMinX, visibleH = worldMaxY - worldMinY; + const showGrid = mapData.showMapInGame !== false && mapData.showMapInGame !== 'false'; + const timeMs = Date.now(); + + if (playScrollBgDrawActive()) { + ctx.fillStyle = '#0a0e22'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawPlayScrollBgFullCanvas(canvas.width, canvas.height); + } else if (gauntletCrownRunwayBgDrawActivePlay()) { + ctx.fillStyle = '#1a1b26'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawGauntletCrownRunwayBgFullCanvasPlay(canvas.width, canvas.height, { + worldMinX, + worldMaxX, + camX, + camY, + zDraw: zDraw, + mapWpx, + mapHpx, + }); + } else if (stackTowerScrollBgDrawActive()) { + ctx.fillStyle = '#0a0e22'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawStackTowerScrollBgFullCanvas(canvas.width, canvas.height, zDraw); + } else if (mapBackgroundImg && mapBackgroundImg.complete && mapBackgroundImg.naturalWidth) { + /* กล้องเห็นพื้นที่นอก [0,map] ได้ — ห้ามสุ่มต้นทาง drawImage เกินขอบภาพ (จะเกิดซ้ำ/เส้นตัด/ซ้อนกันที่ขอบจอ) */ + ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + drawStackTowerLoopSkyFillAboveMap(ctx, worldMinY, zDraw, canvas.width, canvas.height); + const natW = mapBackgroundImg.naturalWidth; + const natH = mapBackgroundImg.naturalHeight; + const bgMinX = Math.max(0, worldMinX); + const bgMaxX = Math.min(mapWpx, worldMaxX); + const bgMinY = Math.max(0, worldMinY); + const bgMaxY = Math.min(mapHpx, worldMaxY); + if (bgMaxX > bgMinX && bgMaxY > bgMinY) { + const srcX = (bgMinX / mapWpx) * natW; + const srcY = (bgMinY / mapHpx) * natH; + const srcW = ((bgMaxX - bgMinX) / mapWpx) * natW; + const srcH = ((bgMaxY - bgMinY) / mapHpx) * natH; + const destX = (bgMinX - camX) * zDraw + canvas.width / 2; + const destY = (bgMinY - camY) * zDraw + canvas.height / 2 + - (isStackTowerMissionUiMapPlay() ? stackTowerWorldScrollScreenY : 0); + const destW = (bgMaxX - bgMinX) * zDraw; + const destH = (bgMaxY - bgMinY) * zDraw; + if (srcW > 0.25 && srcH > 0.25 && destW > 0.25 && destH > 0.25) { + ctx.drawImage(mapBackgroundImg, srcX, srcY, srcW, srcH, destX, destY, destW, destH); + } + } + } else { + ctx.fillStyle = (isSpaceShooter() || isBalloonBoss()) ? '#0a0e22' : '#1a1b26'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + if (isSpaceShooter()) { + ctx.save(); + for (let s = 0; s < 120; s++) { + const sx = (Math.sin(s * 12.9898 + w) * 0.5 + 0.5) * canvas.width; + const sy = (Math.cos(s * 4.1415 + h) * 0.5 + 0.5) * canvas.height; + const br = (s % 4 === 0) ? 1.6 : 0.9; + ctx.fillStyle = s % 7 === 0 ? 'rgba(200, 240, 255, 0.35)' : 'rgba(255, 255, 255, 0.12)'; + ctx.beginPath(); + ctx.arc(sx, sy, br, 0, Math.PI * 2); + ctx.fill(); + } + ctx.restore(); + } else if (isBalloonBoss()) { + ctx.save(); + ctx.fillStyle = 'rgba(30, 45, 120, 0.25)'; + for (let col = 0; col < 24; col++) { + const x = (col + 0.5) * (canvas.width / 24); + for (let r = 0; r < 18; r++) { + const bit = (col * 17 + r * 31 + Math.floor(timeMs / 80)) % 2; + ctx.fillStyle = bit ? 'rgba(0, 255, 200, 0.14)' : 'rgba(180, 200, 255, 0.08)'; + ctx.fillRect(x - 1, (r / 18) * canvas.height + ((timeMs / 40 + col * 3) % 40), 2.2, 10); + } + } + ctx.fillStyle = 'rgba(240, 248, 255, 0.55)'; + ctx.beginPath(); + ctx.moveTo(0, canvas.height); + const baseY = canvas.height * 0.88; + for (let k = 0; k <= 40; k++) { + const bx = (k / 40) * canvas.width; + const by = baseY - Math.abs(Math.sin(k * 1.7 + w)) * canvas.height * 0.08 - (k % 3) * 4; + ctx.lineTo(bx, by); + } + ctx.lineTo(canvas.width, canvas.height); + ctx.closePath(); + ctx.fill(); + ctx.restore(); + } + } + function worldToScreen(wx, wy) { + const sx = (wx - camX) * zDraw + canvas.width / 2; + const sy = (wy - camY) * zDraw + canvas.height / 2 - stackTowerWorldScrollScreenY; + return [sx, sy]; + } + + function drawGridImagePlacementsLayer() { + if (!mapData.gridImagePlacements || !mapData.gridImagePlacements.length || !mapData.gridImageLibrary || !mapData.gridImageLibrary.length) return; + for (let gi = 0; gi < mapData.gridImagePlacements.length; gi++) { + const sp = mapData.gridImagePlacements[gi]; + const wx0 = sp.x * tileSize; + const wy0 = sp.y * tileSize; + const ww = sp.w * tileSize; + const wh = sp.h * tileSize; + if (wx0 + ww <= worldMinX || wx0 >= worldMaxX || wy0 + wh <= worldMinY || wy0 >= worldMaxY) continue; + let gim = mapGridImageImgs[sp.i]; + let carrySpriteAlpha = 1; + if (isQuizCarry()) { + const domOpt = quizCarryOptionIndexForGridSprite(mapData, sp); + const onHub = spriteOverlapsQuizCarryHubArea(mapData, sp); + const meHolding = me && me.quizCarryHeld != null; + let carryVisualActive = false; + if (domOpt != null && quizCarryOptionHeldByAnyone(domOpt)) carryVisualActive = true; + else if (domOpt == null && onHub && meHolding) carryVisualActive = true; + if (carryVisualActive) { + const gh = mapGridImageHeldImgs[sp.i]; + if (gh && gh.complete && gh.naturalWidth) gim = gh; + else carrySpriteAlpha = 0.32; + } + } + if (!gim || !gim.complete || !gim.naturalWidth) continue; + const sx0 = (wx0 - camX) * zDraw + canvas.width / 2; + const sy0 = (wy0 - camY) * zDraw + canvas.height / 2 - stackTowerWorldScrollScreenY; + const sw = ww * zDraw; + const sh = wh * zDraw; + ctx.save(); + if (carrySpriteAlpha < 1) ctx.globalAlpha = carrySpriteAlpha; + ctx.beginPath(); + ctx.rect(sx0, sy0, sw, sh); + ctx.clip(); + ctx.drawImage(gim, sx0, sy0, sw, sh); + ctx.restore(); + ctx.strokeStyle = 'rgba(255,255,255,0.18)'; + ctx.lineWidth = 1; + ctx.strokeRect(sx0 + 0.5, sy0 + 0.5, sw - 1, sh - 1); + } + } + + const startTileX = Math.max(0, Math.floor(worldMinX / tileSize)); + const endTileX = Math.min(w - 1, Math.ceil(worldMaxX / tileSize)); + const startTileY = Math.max(0, Math.floor(worldMinY / tileSize)); + const endTileY = Math.min(h - 1, Math.ceil(worldMaxY / tileSize)); + if (showGrid) { + for (let y = startTileY; y <= endTileY; y++) { + const lane = isFrogger() ? getLane(y) : null; + let rowFill = null; + if (lane) { + if (lane.type === 'goal') rowFill = 'rgba(158,206,106,0.4)'; + else if (lane.type === 'spawn') rowFill = 'rgba(187,154,247,0.35)'; + else if (lane.type === 'road') rowFill = 'rgba(80,70,60,0.6)'; + else if (lane.type === 'water') rowFill = 'rgba(125,207,255,0.5)'; + } else if (isGauntlet()) { + rowFill = (y % 2 === 0) ? 'rgba(247,118,190,0.08)' : 'rgba(180,90,140,0.06)'; + } else if (isStack()) { + rowFill = (y % 2 === 0) ? 'rgba(122, 162, 247, 0.06)' : 'rgba(187, 154, 247, 0.05)'; + } else if (isJumpSurvive()) { + rowFill = (y % 2 === 0) ? 'rgba(90, 200, 255, 0.07)' : 'rgba(60, 140, 200, 0.06)'; + } else if (isSpaceShooter()) { + rowFill = (y % 2 === 0) ? 'rgba(30, 45, 110, 0.22)' : 'rgba(20, 32, 78, 0.28)'; + } else if (isBalloonBoss()) { + rowFill = (y % 2 === 0) ? 'rgba(40, 25, 80, 0.2)' : 'rgba(25, 18, 60, 0.24)'; + } else if (isQuizBattle()) { + rowFill = (y % 2 === 0) ? 'rgba(110, 175, 255, 0.06)' : 'rgba(230, 110, 140, 0.055)'; + } + for (let x = startTileX; x <= endTileX; x++) { + const wx = x * tileSize, wy = y * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const size = tileSize * zDraw; + const ob = mapData.objects?.[y]?.[x] ?? 0; + if (ob === 1) { + ctx.fillStyle = 'rgba(65,72,104,0.92)'; + ctx.fillRect(sx, sy, size, size); + ctx.strokeStyle = '#565f89'; + ctx.strokeRect(sx, sy, size, size); + } else { + const cellColor = showGrid && mapData.cellColors && mapData.cellColors[y] && mapData.cellColors[y][x]; + if (cellColor) { ctx.fillStyle = cellColor; ctx.fillRect(sx, sy, size, size); } + else if (rowFill) { ctx.fillStyle = rowFill; ctx.fillRect(sx, sy, size, size); } + else if (!mapBackgroundImg || !mapBackgroundImg.complete) { + ctx.fillStyle = (x + y) % 2 === 0 ? '#24283b' : '#1f2335'; + ctx.fillRect(sx, sy, size, size); + } + } + } + } + } + drawGridImagePlacementsLayer(); + if (showGrid) { + for (let y = startTileY; y <= endTileY; y++) { + for (let x = startTileX; x <= endTileX; x++) { + const wx = x * tileSize, wy = y * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const size = tileSize * zDraw; + const isInter = mapData.interactive && mapData.interactive[y] && mapData.interactive[y][x] === 1; + if (isInter) { + ctx.fillStyle = 'rgba(158,206,106,0.35)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(158,206,106,0.8)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + if (isQuiz()) { + const isQuizQ = mapData.quizQuestionArea && mapData.quizQuestionArea[y] && mapData.quizQuestionArea[y][x] === 1; + if (isQuizQ) { + ctx.fillStyle = 'rgba(255, 214, 102, 0.32)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(224, 185, 70, 0.78)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const isQuizT = mapData.quizTrueArea && mapData.quizTrueArea[y] && mapData.quizTrueArea[y][x] === 1; + if (isQuizT) { + ctx.fillStyle = 'rgba(86, 202, 255, 0.38)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(122, 220, 255, 0.85)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const isQuizF = mapData.quizFalseArea && mapData.quizFalseArea[y] && mapData.quizFalseArea[y][x] === 1; + if (isQuizF) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.38)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(255, 130, 200, 0.85)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + } + if (isQuizCarry()) { + const hub = mapData.quizCarryHubArea && mapData.quizCarryHubArea[y] && mapData.quizCarryHubArea[y][x] === 1; + if (hub) { + ctx.fillStyle = 'rgba(187, 154, 247, 0.35)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(200, 170, 255, 0.75)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const isCarryQ = mapData.quizQuestionArea && mapData.quizQuestionArea[y] && mapData.quizQuestionArea[y][x] === 1; + if (isCarryQ) { + ctx.fillStyle = 'rgba(255, 214, 102, 0.3)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(224, 185, 70, 0.82)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + const ov = mapData.quizCarryOptionArea && mapData.quizCarryOptionArea[y] && mapData.quizCarryOptionArea[y][x]; + if (ov >= 1 && ov <= QUIZ_CARRY_MAX_OPTION_SLOTS) { + ctx.fillStyle = quizCarryMinimapOptionFillCss(ov); + ctx.fillRect(sx + 3, sy + 3, size - 6, size - 6); + } + } + if (isQuizBattle() && showGrid && mapData.quizBattlePathArea && mapData.quizBattlePathArea[y] && mapData.quizBattlePathArea[y][x] === 1) { + ctx.fillStyle = 'rgba(186, 130, 255, 0.4)'; + ctx.fillRect(sx + 1, sy + 1, size - 2, size - 2); + ctx.strokeStyle = 'rgba(220, 170, 255, 0.55)'; + ctx.strokeRect(sx + 1, sy + 1, size - 2, size - 2); + } + if (isQuizBattle() && showGrid && mapData.quizBattleDomeArea && mapData.quizBattleDomeArea[y] && mapData.quizBattleDomeArea[y][x] === 1) { + const cid = mapData.quizBattleDomeComp && mapData.quizBattleDomeComp[y] && mapData.quizBattleDomeComp[y][x]; + const done = cid > 0 && quizBattleAnsweredComps.has(cid); + ctx.fillStyle = done ? 'rgba(90, 90, 110, 0.4)' : 'rgba(100, 200, 255, 0.36)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = done ? 'rgba(140, 140, 160, 0.65)' : 'rgba(255, 100, 130, 0.82)'; + ctx.lineWidth = 2; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.lineWidth = 1; + } + if (isStack()) { + const sr = mapData.stackReleaseArea, sl = mapData.stackLandArea; + if (sr && sr[y] && sr[y][x] === 1) { + ctx.fillStyle = 'rgba(125, 207, 255, 0.28)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(122, 220, 255, 0.55)'; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + } + if (sl && sl[y] && sl[y][x] === 1) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.26)'; + ctx.fillRect(sx + 3, sy + 3, size - 6, size - 6); + ctx.strokeStyle = 'rgba(255, 130, 200, 0.5)'; + ctx.strokeRect(sx + 3, sy + 3, size - 6, size - 6); + } + } + /* ไม่วาด P1–P6 บนกริดระหว่างเล่น — ตำแหน่งไทล์คงที่ แต่ตัวละครอยู่ world px + บอทขยับ จึงไม่ตรง design · Slot labels belong in Map Editor only */ + if (isSpaceShooter() && showGrid && mapData.shooterSpawnSlots) { + const sv = mapData.shooterSpawnSlots[y] && mapData.shooterSpawnSlots[y][x]; + if (sv >= 1 && sv <= 6) { + ctx.fillStyle = 'rgba(0, 255, 240, 0.14)'; + ctx.fillRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.strokeStyle = 'rgba(120, 255, 255, 0.45)'; + ctx.lineWidth = 1.5; + ctx.strokeRect(sx + 2, sy + 2, size - 4, size - 4); + ctx.lineWidth = 1; + ctx.fillStyle = 'rgba(220, 250, 255, 0.9)'; + ctx.font = `bold ${Math.max(8, 10 * zDraw)}px sans-serif`; + ctx.textAlign = 'center'; + ctx.fillText('P' + sv, sx + size / 2, sy + size / 2 + 4); + ctx.textAlign = 'left'; + } + } + } + } + } + if (isJumpSurvive() && mapData.jumpSurvivePlatformArea) { + const pa = mapData.jumpSurvivePlatformArea; + const sc = jumpSurvivePlatformScrollPx; + const tsz = tileSize; + const period = h * tsz; + const visTop = worldMinY; + const visBot = worldMaxY; + const eps = 1e-6; + for (let py = 0; py < h; py++) { + for (let px = 0; px < w; px++) { + if (!pa[py] || pa[py][px] !== 1) continue; + const vIdx = jumpSurvivePlatformVariantIndexAtPlay(mapData, px, py); + if (px > 0 && pa[py][px - 1] === 1 && jumpSurvivePlatformVariantIndexAtPlay(mapData, px - 1, py) === vIdx) { + continue; + } + let runLen = 1; + while (px + runLen < w && pa[py][px + runLen] === 1 + && jumpSurvivePlatformVariantIndexAtPlay(mapData, px + runLen, py) === vIdx) runLen += 1; + const cfg = playJumpSurvivePlatformTiles[vIdx - 1] || { url: '', w: 0, h: 0 }; + const jumpPlatU = normalizeGauntletAssetUrlForPlay(cfg.url || ''); + const jumpPlatRec = jumpPlatU ? ensureGauntletAssetImage(jumpPlatU) : null; + const jumpPlatImg = jumpPlatRec && jumpPlatRec.ready && jumpPlatRec.img && jumpPlatRec.img.naturalWidth > 0 ? jumpPlatRec.img : null; + const tileWorldLeft = px * tsz; + const tileWorldRight = (px + runLen) * tsz; + if (tileWorldRight <= worldMinX || tileWorldLeft >= worldMaxX) continue; + const rawTop = py * tsz + sc; + const kMin = Math.ceil((visTop - rawTop - tsz + eps) / period); + const kMax = Math.floor((visBot - rawTop - eps) / period); + if (kMax < kMin) continue; + for (let kk = kMin; kk <= kMax; kk++) { + const platTop = rawTop + kk * period; + if (platTop + tsz <= visTop || platTop >= visBot) continue; + const [psx, psy] = worldToScreen(tileWorldLeft, platTop); + const psz = tsz * zDraw; + const segScreenW = runLen * psz; + let dw; + let dh = cfg.h > 0 ? cfg.h * zDraw : psz - 4; + if (runLen > 1) { + dw = segScreenW; + } else { + dw = cfg.w > 0 ? cfg.w * zDraw : psz - 4; + const maxDim = psz * 4; + if (dw > maxDim) dw = maxDim; + } + const maxDh = psz * 4; + if (dh > maxDh) dh = maxDh; + const marginBot = 2; + const dx = runLen > 1 ? psx : psx + (psz - dw) / 2; + const dy = psy + psz - dh - marginBot; + if (jumpPlatImg) { + ctx.save(); + try { ctx.imageSmoothingEnabled = true; } catch (e) { /* ignore */ } + ctx.drawImage(jumpPlatImg, dx, dy, dw, dh); + ctx.restore(); + } else { + const fillW = runLen > 1 ? segScreenW - 6 : psz - 6; + ctx.fillStyle = 'rgba(0, 40, 52, 0.72)'; + ctx.fillRect(psx + 3, psy + 3, fillW, psz - 6); + const grad = ctx.createLinearGradient(psx, psy, psx, psy + psz); + grad.addColorStop(0, 'rgba(0, 255, 240, 0.12)'); + grad.addColorStop(0.5, 'rgba(0, 0, 0, 0)'); + grad.addColorStop(1, 'rgba(0, 180, 200, 0.08)'); + ctx.fillStyle = grad; + ctx.fillRect(psx + 4, psy + 4, fillW - 2, psz - 8); + ctx.lineWidth = 1; + if (zDraw >= 1.15 && psz > 22) { + ctx.save(); + ctx.font = `600 ${Math.max(7, Math.min(11, psz * 0.14))}px "Share Tech Mono", ui-monospace, monospace`; + ctx.fillStyle = 'rgba(180, 255, 255, 0.5)'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + const cx = psx + (runLen > 1 ? segScreenW * 0.5 : psz * 0.5); + ctx.fillText('DATA', cx, psy + psz * 0.38); + ctx.font = `500 ${Math.max(5, Math.min(8, psz * 0.1))}px "Share Tech Mono", ui-monospace, monospace`; + ctx.fillStyle = 'rgba(120, 230, 255, 0.38)'; + ctx.fillText('/SEC/BLOCK', cx, psy + psz * 0.62); + ctx.restore(); + } + } + } + } + } + } + if (isJumpSurvive() && showGrid && mapData.jumpSurviveHazardArea) { + const ha = mapData.jumpSurviveHazardArea; + const tsz = tileSize; + for (let py = startTileY; py <= endTileY; py++) { + for (let px = startTileX; px <= endTileX; px++) { + if (!ha[py] || ha[py][px] !== 1) continue; + const [psx, psy] = worldToScreen(px * tsz, py * tsz); + const psz = tsz * zDraw; + ctx.fillStyle = 'rgba(200, 28, 55, 0.38)'; + ctx.fillRect(psx + 1, psy + 1, psz - 2, psz - 2); + ctx.strokeStyle = 'rgba(255, 90, 110, 0.82)'; + ctx.lineWidth = 2; + ctx.strokeRect(psx + 1, psy + 1, psz - 2, psz - 2); + ctx.lineWidth = 1; + } + } + } + if (showGrid) { + if (isFrogger() && mapData.lanes) { + for (let y = startTileY; y <= endTileY; y++) { + const lane = getLane(y); + if (!lane || (lane.type !== 'road' && lane.type !== 'water')) continue; + const positions = getVehiclePositions(lane, mapData.width, timeMs); + const isRoad = lane.type === 'road'; + for (let i = 0; i < positions.length; i++) { + const vx = positions[i]; + const wx = vx * tileSize, wy = y * tileSize; + const [sx, sy] = worldToScreen(wx, wy); + const size = tileSize * zDraw * (isRoad ? 1.2 : 1.5); + ctx.fillStyle = isRoad ? '#e0a060' : '#8b7355'; + ctx.fillRect(sx, sy, size, size * 0.7); + ctx.strokeStyle = isRoad ? '#c0caf5' : '#9ece6a'; + ctx.strokeRect(sx, sy, size, size * 0.7); + } + } + } + } + + if (isQuizCarry()) { + drawQuizCarryChoiceLabels(ctx, worldToScreen, zDraw); + } + + if (isStack() && stackMini) { + drawStackMinigame(ctx, worldToScreen, zDraw); + } + + const gauntletObsDraw = (isGauntlet() && !gauntletCrownRunwayBgHideObstaclesPlay()) + ? getGauntletObsDrawPositionsAt(performance.now()) + : []; + if (isGauntlet() && gauntletObsDraw.length) { + const stx = Math.max(0, Math.floor(worldMinX / tileSize)); + const enx = Math.min(w - 1, Math.ceil(worldMaxX / tileSize)); + const sty = Math.max(0, Math.floor(worldMinY / tileSize)); + const eny = Math.min(h - 1, Math.ceil(worldMaxY / tileSize)); + for (let i = 0; i < gauntletObsDraw.length; i++) { + const o = gauntletObsDraw[i]; + if (!o) continue; + if (o.kind === 'lane' && typeof o.y === 'number') { + if (o.drawX < stx - 2 || o.drawX > enx + 2 || o.y < sty || o.y > eny) continue; + const size = tileSize * zDraw; + const inner = Math.max(2, size - 4); + const [sxC, syCellBottom] = worldToScreen((o.drawX + 0.5) * tileSize, (o.y + 1) * tileSize); + const dx = sxC - inner / 2; + const dy = syCellBottom - inner; + const laneRec = pickGauntletLaneImageRec(o.id); + if (laneRec && laneRec.img.complete && laneRec.img.naturalWidth > 0) { + try { + ctx.drawImage(laneRec.img, dx, dy, inner, inner); + } catch (e) { + ctx.fillStyle = '#f7768e'; + ctx.fillRect(dx, dy, inner, inner); + } + } else { + ctx.fillStyle = '#f7768e'; + ctx.fillRect(dx, dy, inner, inner); + } + } else if (o.kind === 'laser' && typeof o.drawX === 'number') { + if (o.drawX < stx - 2 || o.drawX > enx + 2) continue; + const { y0: laserRow0, y1: laserRow1 } = gauntletLaserRowHitRange(o, h); + const wx0 = o.drawX * tileSize; + const wx1 = (o.drawX + 1) * tileSize; + const wy0 = laserRow0 * tileSize; + const wy1 = (laserRow1 + 1) * tileSize; + const [sx0, syTop] = worldToScreen(wx0, wy0); + const [sx1, syBot] = worldToScreen(wx1, wy1); + const rx = Math.min(sx0, sx1); + const rw = Math.max(2, Math.abs(sx1 - sx0)); + const ry = Math.min(syTop, syBot); + const rh = Math.abs(syBot - syTop); + drawGauntletLaserColumnScreen(rx, ry, rw, rh); + } + } + } + + /** ป้ายติดตัว — กึ่งกลางลำตัว (ทับอก/เอว) แบบเดียวกับป้ายบนพื้น ไม่มีเส้นเชื่อม */ + function drawQuizCarryHeldSignBoard(sxFeet, feetSy, halfSpriteW, spriteTopY, signText, _faceDir, choiceIndex, imageUrl, imageElementOpt) { + const imgUrlSan = sanitizeQuizCarryImageUrlClient(imageUrl); + const gridHeldDraw = imageElementOpt && imageElementOpt.complete && imageElementOpt.naturalWidth > 0 ? imageElementOpt : null; + const raw = String(signText || '').trim(); + if (!raw && !imgUrlSan && !gridHeldDraw) return; + ctx.save(); + const heldPs = quizCarryPlaqueMapScaleClampedPlay(); + const bodyH = Math.max(18, feetSy - spriteTopY); + const midBodyY = spriteTopY + bodyH * 0.48; + const bodyCx = sxFeet; + const halfW = Math.max(10, halfSpriteW || 0); + const baseMaxW = Math.min(280, Math.max(100, halfW * 2.4 + tileSize * zDraw * 0.5)); + const maxPlaqueW = Math.min(340, Math.round(baseMaxW * heldPs)); + + const fontPx = Math.max(12, Math.min(22, tileSize * zDraw * 0.22 * heldPs)); + ctx.font = `600 ${fontPx}px system-ui, "Segoe UI", "Kanit", sans-serif`; + const padX = 12; + const padY = 9; + const maxInner = Math.min(maxPlaqueW - padX * 2, Math.max(72, tileSize * zDraw * 4.2)); + let lines; + if (!raw) { + lines = []; + } else { + lines = canvasWordWrapLines(ctx, raw, maxInner); + if (!lines.length) lines = [raw]; + if (lines.length > 3) lines = lines.slice(0, 3); + for (let i = 0; i < lines.length; i++) { + let s = lines[i]; + if (ctx.measureText(s).width <= maxInner) continue; + while (s.length > 2 && ctx.measureText(s + '…').width > maxInner) s = s.slice(0, -1); + lines[i] = s + '…'; + } + } + let maxLineW = 0; + for (let i = 0; i < lines.length; i++) { + const lw = ctx.measureText(lines[i]).width; + if (lw > maxLineW) maxLineW = lw; + } + const lineH = fontPx * 1.22; + let w; + let h; + if (!lines.length && (imgUrlSan || gridHeldDraw)) { + w = Math.ceil(Math.min(maxPlaqueW, Math.max(104, maxInner + padX * 2))); + h = Math.ceil(Math.max(56, fontPx * 2.8 + padY * 2)); + } else { + w = Math.ceil(Math.max(maxLineW, 56) + padX * 2); + h = Math.ceil(lines.length * lineH + padY * 2); + w = Math.min(maxPlaqueW, Math.max(96, w)); + h = Math.max(40, h); + } + + let signX = bodyCx - w / 2; + let signY = midBodyY - h / 2; + + if (signX + w > canvas.width - 6) signX = canvas.width - w - 6; + if (signX < 6) signX = 6; + if (signY < 6) signY = 6; + if (signY + h > feetSy + 8) signY = feetSy + 8 - h; + + drawQuizCarryNeonPlaque(ctx, signX, signY, w, h, lines, lineH, padY, choiceIndex, null, { imageUrl: imgUrlSan, imageElement: gridHeldDraw }); + ctx.restore(); + } + + function drawAvatar(ax, ay, isMe, name, characterId, direction, isWalking, playTint, gauntletAirTicks, gauntletScoreLabel, quizCarrySignPayload, crownPenaltyFxUntil) { + const { cw, ch } = getCharacterFootprintWH(mapData); + const air = Number(gauntletAirTicks) || 0; + let liftWorldY = 0; + if (isGauntlet() && air > 0) { + liftWorldY = gauntletLiftHeightNorm(air, gauntletRuntimeJumpTicks) * tileSize * 0.52; + } + const cxWorld = (ax + cw * 0.5) * tileSize; + /* ใช้ ay แบบต่อเนื่อง — ห้าม floor(ay) เดิมทำให้เท้ากระโดดทีละช่องขณะเดิน (กระตุกแนวตั้ง) */ + const cyBottomWorld = (ay + ch) * tileSize - liftWorldY; + const [sx, sy] = worldToScreen(cxWorld, cyBottomWorld); + const cellSpan = Math.max(cw, ch); + const r = Math.max(14, (tileSize * zDraw * cellSpan) / 2 - 2); + const size = r * 2.2; + const dir = direction || 'down'; + const rawImg = getAvatarImg(characterId, dir, timeMs, isWalking); + const charImg = playTint + ? getPlayTintedAvatarSource(rawImg, characterId, dir, timeMs, isWalking, playTint) + : rawImg; + /* รองรับทั้ง และ canvas จากย้อมสี (canvas ไม่มี naturalWidth → เดิมตกวงกลม) */ + let iw = 0, ih = 0; + if (charImg && charImg.tagName === 'CANVAS' && charImg.width > 0 && charImg.height > 0) { + iw = charImg.width; + ih = charImg.height; + } else if (charImg && charImg.complete && charImg.naturalWidth) { + iw = charImg.naturalWidth; + ih = charImg.naturalHeight; + } + let halfSpriteW = r; + let spriteTopY = sy - 2 * r; + if (iw > 0 && ih > 0) { + const scale = Math.min(size / iw, size / ih, 1); + const drawW = iw * scale; + const drawH = ih * scale; + halfSpriteW = drawW / 2; + spriteTopY = sy - drawH; + ctx.drawImage(charImg, 0, 0, iw, ih, sx - drawW / 2, sy - drawH, drawW, drawH); + } else { + const cy = sy - r; + ctx.fillStyle = isMe ? '#7aa2f7' : '#9ece6a'; + ctx.beginPath(); + ctx.arc(sx, cy, r, 0, Math.PI * 2); + ctx.fill(); + ctx.strokeStyle = '#c0caf5'; + ctx.lineWidth = 2; + ctx.stroke(); + spriteTopY = cy - r; + } + ctx.fillStyle = '#c0caf5'; + ctx.font = '10px sans-serif'; + ctx.textAlign = 'center'; + const nameBase = name || ''; + const withScore = isGauntlet() && typeof gauntletScoreLabel === 'number' && gauntletScoreLabel >= 0 + ? (nameBase + ' · ' + gauntletScoreLabel) + : nameBase; + ctx.fillText(withScore, sx, sy + 10); + if (isGauntletCrownHeistMapPlay() + && typeof crownPenaltyFxUntil === 'number' + && crownPenaltyFxUntil > timeMs) { + const penImg = ensureGauntletCrownScorePenaltyImgPlay(); + if (penImg && penImg.complete && penImg.naturalWidth > 0) { + const ref = Math.max(18, r * 2); + const scale = Math.min(1.15, ref / Math.max(penImg.naturalWidth, penImg.naturalHeight, 1)); + const dw = penImg.naturalWidth * scale; + const dh = penImg.naturalHeight * scale; + ctx.drawImage(penImg, 0, 0, penImg.naturalWidth, penImg.naturalHeight, sx - dw * 0.5, spriteTopY - dh - 5, dw, dh); + } + } + if (isQuizCarry() && quizCarrySignPayload) { + drawQuizCarryHeldSignBoard( + sx, sy, halfSpriteW, spriteTopY, + quizCarrySignPayload.text, dir, quizCarrySignPayload.choiceIndex, + quizCarrySignPayload.imageUrl, + quizCarrySignPayload.imageElement, + ); + } + } + function quizCarrySignForEntity(ent) { + if (!isQuizCarry() || !quizCarryCurrent || !ent || ent.quizCarryHeld == null) return null; + const idx = ent.quizCarryHeld; + const ch = quizCarryCurrent.choices; + if (!Array.isArray(ch) || idx < 0 || idx >= ch.length) return null; + const t = String(ch[idx] != null ? ch[idx] : '').trim(); + const imgUrl = getQuizCarryPlaqueImageUrlForIndex(quizCarryCurrent, idx); + const gridHeldEl = findQuizCarryGridHeldImgForChoiceIndex(idx); + if (!t && !imgUrl && !gridHeldEl) return null; + return { text: t, choiceIndex: idx, imageUrl: imgUrl, imageElement: gridHeldEl || undefined }; + } + const safeX = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1); + const safeY = (v) => (typeof v === 'number' && !isNaN(v) ? v : 1); + function peerVisualOffset(id) { + let h = 0; + for (let i = 0; i < (id || '').length; i++) h = (h * 31 + (id || '').charCodeAt(i)) >>> 0; + return { ax: ((h % 5) - 2) * 0.1, ay: ((Math.floor(h / 5) % 5) - 2) * 0.1 }; + } + function shouldGauntletCrownHeistSkipAvatarDrawPlay(ent, isMeFlag, offAx, offAy) { + if (!isGauntletCrownHeistMapPlay() || gauntletCrownPregamePhase !== 'live') return false; + if (!ent || ent.gauntletEliminated) return true; + const { cw, ch } = getCharacterFootprintWH(mapData); + const ax = safeX(ent.x) + (offAx || 0); + const ay = safeY(ent.y) + (offAy || 0); + const airTicks = isMeFlag ? (meGauntletJumpTicks || 0) : (Number(ent.gauntletJumpTicks) || 0); + const airVis = isMeFlag ? (meGauntletJumpVis || 0) : (Number(ent.gauntletJumpVis) || 0); + const air = airVis > 0.08 ? airVis : airTicks; + let liftWorldY = 0; + if (isGauntlet() && air > 0) { + liftWorldY = gauntletLiftHeightNorm(air, gauntletRuntimeJumpTicks) * tileSize * 0.52; + } + const cxWorld = (ax + cw * 0.5) * tileSize; + const cyBottomWorld = (ay + ch) * tileSize - liftWorldY; + const [sx, sy] = worldToScreen(cxWorld, cyBottomWorld); + const cellSpan = Math.max(cw, ch); + const r = Math.max(14, (tileSize * zDraw * cellSpan) / 2 - 2); + const pad = Math.max(28, r * 0.4); + if (sx + r + pad < 0 || sx - r - pad > canvas.width) return true; + if (sy + 52 < 0 || sy - r * 4 - pad > canvas.height) return true; + return false; + } + const { ch: sortCh } = getCharacterFootprintWH(mapData); + /** ใช้แถวกริด (floor y) เป็นหลัก — ลดการสลับลำดับวาดทุกเฟรมเมื่อ y เป็น float เลอร์ป */ + function avatarDrawDepth(ent) { + return Math.floor(safeY(ent.y)) + sortCh; + } + if (!isStack() && !isSpaceShooter() && !isBalloonBoss()) { + let mt = me.playTint; + if (!mt || typeof mt.head !== 'string' || typeof mt.hair !== 'string' || typeof mt.body !== 'string') { + mt = playTintFromPeerId(String((myId != null && myId !== '') ? myId : (nick || 'local'))); + me.playTint = mt; + } + const meTag = isFrogger() ? (' (กบ) ' + froggerScore) : (isGauntlet() && meGauntletJumpTicks > 0 ? ' (กระโดด)' : (isJumpSurvive() && jumpSurviveEliminated ? ' (ตกรอบ)' : ' (คุณ)')); + const crownRunStripLive = isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live' && gauntletCrownRunwayAvatarRunAllowedPlay(); + const meWalking = isFrogger() + ? false + : (isGauntlet() + ? ((crownRunStripLive && !me.gauntletEliminated) || meGauntletJumpTicks > 0 || meGauntletJumpVis > 0.08) + : !!me.isWalking); + const drawRows = []; + others.forEach((o, id) => { + if (!o) return; + drawRows.push({ isMe: false, id, o }); + }); + drawRows.push({ isMe: true, id: '__me', o: me }); + drawRows.sort((a, b) => { + const da = avatarDrawDepth(a.o); + const db = avatarDrawDepth(b.o); + if (da !== db) return da - db; + const xa = Math.floor(safeX(a.o.x) * 4); + const xb = Math.floor(safeX(b.o.x) * 4); + if (xa !== xb) return xa - xb; + const sa = a.isMe ? '\uFFFFme' : String(a.id); + const sb = b.isMe ? '\uFFFFme' : String(b.id); + return sa < sb ? -1 : sa > sb ? 1 : 0; + }); + drawRows.forEach((row) => { + if (!row.isMe) { + const id = row.id; + const o = row.o; + const off = isGauntlet() ? { ax: 0, ay: 0 } : peerVisualOffset(id); + const otherWalk = isGauntlet() + ? ((crownRunStripLive && !o.gauntletEliminated) || (o.gauntletJumpTicks || 0) > 0 || (o.gauntletJumpVis || 0) > 0.08) + : (isPreviewBotId(id) + ? !!o.botIsWalking + : !!((o.tx != null && Math.abs((o.tx || o.x) - o.x) > 0.02) || (o.ty != null && Math.abs((o.ty || o.y) - o.y) > 0.02))); + const ot = o.playTint || playTintFromPeerId(id); + const faceDirOther = isGauntletFaceRightMapMno9kb07() ? 'right' : o.direction; + const botOut = isJumpSurvive() && isPreviewBotId(id) && o.jumpSurviveEliminated; + const peerName = botOut ? (o.nickname + ' (ตกรอบ)') : o.nickname; + if (shouldGauntletCrownHeistSkipAvatarDrawPlay(o, false, off.ax, off.ay)) return; + if (botOut) ctx.save(); + if (botOut) ctx.globalAlpha = 0.4; + drawAvatar(safeX(o.x) + off.ax, safeY(o.y) + off.ay, false, peerName, o.characterId, faceDirOther, otherWalk, ot, (o.gauntletJumpVis != null ? o.gauntletJumpVis : o.gauntletJumpTicks) || 0, o.gauntletScore || 0, quizCarrySignForEntity(o), isGauntletCrownHeistMapPlay() ? (o.gauntletCrownPenaltyFxUntil || 0) : 0); + if (botOut) ctx.restore(); + } else { + if (shouldGauntletCrownHeistSkipAvatarDrawPlay(me, true, 0, 0)) return; + if (isJumpSurvive() && jumpSurviveEliminated) ctx.save(); + if (isJumpSurvive() && jumpSurviveEliminated) ctx.globalAlpha = 0.4; + const faceDirMe = isGauntletFaceRightMapMno9kb07() ? 'right' : me.direction; + drawAvatar(safeX(me.x), safeY(me.y), true, me.nickname + meTag, me.characterId, faceDirMe, meWalking, mt, meGauntletJumpVis, me.gauntletScore || 0, quizCarrySignForEntity(me), isGauntletCrownHeistMapPlay() ? (me.gauntletCrownPenaltyFxUntil || 0) : 0); + if (isJumpSurvive() && jumpSurviveEliminated) ctx.restore(); + } + }); + } + if (isFrogger()) { + ctx.fillStyle = '#7aa2f7'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('โหมดกบ | คะแนน: ' + froggerScore, 10, 24); + } + if (isJumpSurvive() && !useCyberPlayHud()) { + ctx.fillStyle = 'rgba(148, 226, 213, 0.95)'; + ctx.font = '600 12px system-ui, "Segoe UI", "Kanit", sans-serif'; + ctx.textAlign = 'left'; + if (jumpSurviveEliminated) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.95)'; + ctx.fillText('คุณตกรอบแล้ว (ชนหรือหลุดเพดาน/พื้นจอ) · เล่นต่อไม่ได้ — ดูเพื่อนได้ · รอรอบใหม่หรือออกจากห้อง', 10, 22); + } else { + ctx.fillText('กระโดดให้รอด · Space / W / ↑ · A D / ← → · แพลตฟอร์มวน · ชนหรือหลุดบน/ล่างจอ = ตกรอบ (ดูเพื่อนต่อ)', 10, 22); + } + } + if (isGauntlet() && !useCyberPlayHud()) { + ctx.fillStyle = '#f7768e'; + ctx.font = 'bold 14px sans-serif'; + ctx.textAlign = 'left'; + if (isGauntletCrownHeistMapPlay()) { + ctx.fillText('พรมแดงสุดท้าย | คะแนน ' + (me.gauntletScore || 0) + ' · ชน -10 · ขอบซ้ายตาย · สูงสุด 6 คน', 10, 24); + ctx.font = '12px sans-serif'; + ctx.fillStyle = '#c0caf5'; + ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · จบเกมมีโบนัสอันดับ + เกรดทีม', 10, 42); + } else { + ctx.fillText('พรมแดงสุดท้าย | คะแนน: ' + (me.gauntletScore || 0) + ' (ข้าม 1 ชิ้น +1) · สูงสุด 6 คน', 10, 24); + ctx.font = '12px sans-serif'; + ctx.fillStyle = '#c0caf5'; + ctx.fillText('Space / W / ↑ = กระโดด · เลน + เลเซอร์ · ข้ามสำเร็จ → ขวา 1 + คะแนน · ชน → ซ้าย 1', 10, 42); + } + ctx.fillStyle = '#e0af68'; + if (!(isGauntletCrownHeistMapPlay() && gauntletCrownRunwayBgDrawActivePlay())) { + if (gauntletEndsAtMs != null && Number.isFinite(gauntletEndsAtMs)) { + const rem = Math.max(0, Math.ceil((gauntletEndsAtMs - Date.now()) / 1000)); + const mm = Math.floor(rem / 60); + const ss = rem % 60; + const clock = `${mm}:${String(ss).padStart(2, '0')}`; + ctx.fillText(`เหลือเวลา ${clock} · Time left ${clock}`, 10, 60); + } else if (gauntletRuntimeTimeLimitSec > 0) { + ctx.fillText('เวลา: กำลังซิงก์จากเซิร์ฟเวอร์... · Syncing timer...', 10, 60); + } else { + ctx.fillText('เวลา: ไม่จำกัด · No time limit (ตั้งได้ที่ Admin → เวลาเกม)', 10, 60); + } + } + } + if (isStack() && stackMini) { + if (playBotsEnabled()) { + ctx.fillStyle = 'rgba(125, 220, 255, 0.9)'; + ctx.font = '600 12px ui-sans-serif, system-ui, sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Stack · shared tower · Space / click = release on your turn', 10, 22); + drawStackPreviewCyberHud(ctx, canvas.width, canvas.height); + } else { + ctx.fillStyle = '#7dcfff'; + ctx.font = 'bold 15px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Stack | คะแนน ' + stackMini.score + ' · ชีวิต ' + stackMini.lives + ' · combo x' + (stackMini.combo || 0), 10, 24); + ctx.font = '12px sans-serif'; + ctx.fillStyle = '#c0caf5'; + ctx.fillText('Space / คลิก = ปล่อย', 10, 44); + } + } + if (isQuiz() || isQuizCarry() || isQuizBattle()) { + syncPlayQuizMapPanel(); + syncQuizCarryEmbedQuestionStrip(); + } + /* timeup ต้องซิงก์ทุกเฟรมแม้จบเซสชัน — อย่าให้อยู่ในเงื่อนไขด้านบนอย่างเดียว */ + syncQuizCarryTimeupDeskLayerPosition(); + if (playBotsEnabled() && mapData && !useCyberPlayHud()) { + const human = countPlayHumans(); + const bots = [...others.keys()].filter(isPreviewBotId).length; + ctx.save(); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.fillStyle = 'rgba(26,27,38,0.78)'; + const hudW = isStack() ? Math.min(canvas.width - 12, 520) : Math.min(canvas.width - 12, 320); + ctx.fillRect(6, canvas.height - 38, hudW, 30); + ctx.fillStyle = '#a9b1d6'; + ctx.font = '12px sans-serif'; + ctx.textAlign = 'left'; + const botTarget = playBotTargetHeadcount(); + let line = detectiveCaseFillBots + ? ('คดีสืบสวน: คนจริง ' + human + ' + บอท ' + bots + ' (เป้า ' + botTarget + ' คน)') + : ('ทดสอบ: คนจริง ' + human + ' + บอท ' + bots + ' (เป้า ' + botTarget + ' คน)'); + if (isStack()) { + line += ' · ตึกร่วม + HUD ซ้าย/ขวา'; + } + ctx.fillText(line, 12, canvas.height - 18); + ctx.restore(); + } + if (isSpaceShooter()) { + drawSpaceShooterCombatLayer(ctx, worldToScreen, zDraw, timeMs); + } + if (isBalloonBoss()) { + drawBalloonBossCombatLayer(ctx, worldToScreen, zDraw, timeMs); + } + if (isQuizCarry()) { + drawQuizCarryEmbedCountdownHighlight(ctx, worldToScreen, zDraw, timeMs); + } + if (previewMode && editorEmbedReturn && isQuizCarry() + && (quizCarryEmbedCountdownEndAt > Date.now() || quizCarryEmbedPreOptionCountdownEndAt > Date.now())) { + syncQuizCarryEmbedCountdownLayout(); + } + drawQuizTfScorePopupsLayer(ctx, worldToScreen, zDraw, timeMs); + syncPlayCyberHud(); + syncQuizQuestionMissionJoystickPlay(); + syncQuizCarryGrabButton(); + syncGauntletCrownJumpButton(); + syncStackTowerDropButtonPlay(); + } + + document.addEventListener('keydown', (e) => { + if (isMovementKey(e.code) && isChatFocused()) return; + if (previewMode && editorEmbedReturn && mapData && !isChatFocused() && !isQuizQuestionMissionHudActivePlay() && !isStackTowerEmbedZoomLockedPlay()) { + if (e.code === 'BracketLeft' || e.code === 'Minus' || e.code === 'NumpadSubtract') { + e.preventDefault(); + playEmbedUserZoomMul = Math.max(PLAY_EMBED_USER_ZOOM_MIN, playEmbedUserZoomMul / PLAY_EMBED_ZOOM_STEP_KEY); + draw(); + return; + } + if ( + e.code === 'BracketRight' + || e.code === 'NumpadAdd' + || (e.code === 'Equal' && e.shiftKey) + ) { + e.preventDefault(); + playEmbedUserZoomMul = Math.min(PLAY_EMBED_USER_ZOOM_MAX, playEmbedUserZoomMul * PLAY_EMBED_ZOOM_STEP_KEY); + draw(); + return; + } + } + if (isSpaceShooter() && mapData && !isChatFocused() && isSpaceShooterMissionPregameBlockingPlay()) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + } + return; + } + if (isQuiz() && mapData && !isChatFocused() && isQuizQuestionMissionPregameBlockingPlay()) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + } + return; + } + if (isStack() && mapData && !isChatFocused() && isStackTowerMissionPregameBlockingPlay()) { + if (['Space', 'Enter', 'NumpadEnter', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + } + return; + } + if (isBalloonBoss() && isMegaVirusMissionShellMapPlay() && isGauntletCrownPregameBlockingPlay() && mapData && !isChatFocused()) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + } + return; + } + if ((isSpaceShooter() || isBalloonBoss()) && mapData && !isChatFocused()) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + } + } + if (isStack() && mapData && !isChatFocused()) { + if (e.code === 'Space' || e.code === 'Enter' || e.code === 'NumpadEnter') { + e.preventDefault(); + tryHumanStackDrop(); + return; + } + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + return; + } + } + if (isGauntlet() && mapData && !isChatFocused()) { + if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { + e.preventDefault(); + tryRequestGauntletJumpPlay(); + return; + } + if (['ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) { + e.preventDefault(); + return; + } + } + if (isJumpSurvive() && mapData && !isChatFocused()) { + if (isJumpSurviveMissionPregameBlockingPlay()) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) e.preventDefault(); + return; + } + if (jumpSurviveEliminated) { + if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'KeyW', 'KeyA', 'KeyS', 'KeyD'].includes(e.code)) e.preventDefault(); + return; + } + if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'KeyW') { + e.preventDefault(); + if (!e.repeat) jumpSurviveJumpQueued = true; + return; + } + if (e.code === 'ArrowDown' || e.code === 'KeyS') { + e.preventDefault(); + return; + } + } + if (isQuizBattle() && mapData && !isChatFocused()) { + if (e.code === 'KeyE') { + e.preventDefault(); + if (!e.repeat && myId != null) tryOpenQuizBattleFromKey(); + return; + } + } + if (isQuizCarry() && mapData && !isChatFocused()) { + if (e.code === 'KeyF') { + e.preventDefault(); + if (!e.repeat && myId != null) tryQuizCarryInteractionForPlayer(myId, me.x, me.y, { fromKey: true }); + return; + } + } + keys[e.code] = true; + keys[e.key] = true; + if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code)) e.preventDefault(); + if (isFrogger() && mapData && !isChatFocused()) { + const now = Date.now(); + if (now - lastFroggerKey < 280) return; + let dx = 0, dy = 0; + if (e.code === 'ArrowUp' || e.code === 'KeyW') { dy = -1; me.direction = 'up'; } + else if (e.code === 'ArrowDown' || e.code === 'KeyS') { dy = 1; me.direction = 'down'; } + else if (e.code === 'ArrowLeft' || e.code === 'KeyA') { dx = -1; me.direction = 'left'; } + else if (e.code === 'ArrowRight' || e.code === 'KeyD') { dx = 1; me.direction = 'right'; } + if (dx !== 0 || dy !== 0) { + const nx = Math.round(me.x) + dx, ny = Math.round(me.y) + dy; + if (nx >= 0 && nx < mapData.width && ny >= 0 && ny < mapData.height) { + const row = mapData.objects && mapData.objects[ny]; + if (!row || row[nx] !== 1) { + me.x = nx; me.y = ny; + lastFroggerKey = now; + socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); + const lane = getLane(Math.floor(me.y)); + if (lane && lane.type === 'goal') { froggerScore++; respawnFrogger(); } + else if (checkFroggerCollision()) respawnFrogger(); + } + } + } + } + }); + document.addEventListener('keyup', (e) => { keys[e.code] = false; keys[e.key] = false; }); + + let lastSend = 0; + function tick() { + if (!mapData) { requestAnimationFrame(tick); return; } + if (isStack()) { + if (isStackTowerEmbedZoomLockedPlay()) { + playEmbedUserZoomMul = PLAY_EMBED_STACK_TOWER_FIXED_ZOOM_MUL; + } + const nowStBg = performance.now(); + const dtStBg = Math.min(0.06, (nowStBg - lastStackTowerScrollBgTickMs) / 1000); + lastStackTowerScrollBgTickMs = nowStBg; + tickStackTowerScrollBg(dtStBg); + if (isStackTowerMissionPregameBlockingPlay()) { + updateStackTowerSwingYForStripGapPlay(); + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + stackTickFrame(); + updateStackTowerSwingYForStripGapPlay(); + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + if (isJumpSurvive()) { + if (isJumpSurviveMissionPregameBlockingPlay()) { + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + jumpSurviveTickFrame(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isQuiz() && isQuizQuestionMissionUiMapPlay() && isQuizQuestionMissionPregameBlockingPlay()) { + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + if (isSpaceShooter()) { + if (isSpaceShooterMissionPregameBlockingPlay()) { + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + spaceShooterTickFrame(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isBalloonBoss()) { + balloonBossTickFrame(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isFrogger()) { + if (checkFroggerCollision()) respawnFrogger(); + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + if (isGauntlet()) { + { + const nowBg = performance.now(); + const dtBg = Math.min(0.06, lastGauntletCrownRunwayBgTickMs ? (nowBg - lastGauntletCrownRunwayBgTickMs) / 1000 : 0.016); + lastGauntletCrownRunwayBgTickMs = nowBg; + tickGauntletCrownRunwayBgPlay(dtBg); + } + if (isGauntletCrownPregameBlockingPlay()) { + me.isWalking = false; + draw(); + requestAnimationFrame(tick); + return; + } + me.isWalking = (isGauntletCrownHeistMapPlay() && gauntletCrownPregamePhase === 'live' && !me.gauntletEliminated && gauntletCrownRunwayAvatarRunAllowedPlay()) || meGauntletJumpTicks > 0; + const stepGauntletJumpVis = (vis, tgt) => { + const t = Number(tgt) || 0; + let v = Number(vis) || 0; + const d = t - v; + const k = d < 0 ? 0.68 : 0.4; + return v + d * k; + }; + meGauntletJumpVis = stepGauntletJumpVis(meGauntletJumpVis, meGauntletJumpTicks); + const mp = lerpGauntletEntityPos(me.x, me.y, me.tx, me.ty); + me.x = mp.nx; + me.y = mp.ny; + others.forEach((o) => { + const op = lerpGauntletEntityPos(o.x, o.y, o.tx, o.ty); + o.x = op.nx; + o.y = op.ny; + if (o.gauntletJumpVis == null) o.gauntletJumpVis = o.gauntletJumpTicks || 0; + o.gauntletJumpVis = stepGauntletJumpVis(o.gauntletJumpVis, o.gauntletJumpTicks || 0); + }); + clampPlayEntityFootprintToMap(me, mapData); + others.forEach((o) => { + if (o) clampPlayEntityFootprintToMap(o, mapData); + }); + let zGa = zoom; + if (previewMode && editorEmbedReturn && mapData) zGa *= playEmbedUserZoomMul; + const gCam = getGauntletCrownHeistGroupCameraCenterPxPlay(tileSize, canvas.width, canvas.height, zGa); + if (gCam) maybeEliminateGauntletPreviewBotsTailOffCameraPlay(gCam.px, gCam.py, zGa); + draw(); + requestAnimationFrame(tick); + return; + } + /* บอท preview ขยับที่ o.x/o.y โดยตรง — ห้าม LERP กับ tx/ty เพราะ stepPreviewBots เคยตั้ง tx=ก่อนเดิน ทำให้โดนดึงถอย ~20%/เฟรม กระตุกและแทบไม่ไป */ + others.forEach((o, id) => { + if (playBotsEnabled() && isPreviewBotId(id)) return; + if (o.tx != null) o.x += (o.tx - o.x) * LERP; + if (o.ty != null) o.y += (o.ty - o.y) * LERP; + }); + tickQuizCarryEmbedCountdown(); + tickQuizCarryRoundTimers(); + updateQuizCarryCarryPhaseHud(); + stepPreviewBots(); + if (isChatFocused()) { + me.isWalking = false; + enforceQuizBattleLaneOnMePlay(); + draw(); + requestAnimationFrame(tick); + return; + } + if (isQuizCarryEmbedLobbyBlockingMovement() || isQuizCarryEmbedCountdownBlockingMovement()) { + me.isWalking = false; + playPath = []; + enforceQuizBattleLaneOnMePlay(); + draw(); + requestAnimationFrame(tick); + return; + } + const w = mapData.width || 20, h = mapData.height || 15; + let accX = 0, accY = 0; + let usePath = false; + const keyPressed = keys['ArrowUp'] || keys['KeyW'] || keys['ArrowDown'] || keys['KeyS'] || keys['ArrowLeft'] || keys['KeyA'] || keys['ArrowRight'] || keys['KeyD']; + if (playPath.length > 0 && keyPressed) playPath = []; + if (playPath.length > 0) { + const way = playPath[0]; + const dx = way.x - me.x, dy = way.y - me.y; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist <= PATH_ARRIVE_THRESH) { + playPath.shift(); + /* ข้าม waypoint ที่ซ้ำตำแหน่ง — กัน acc=0 ทั้งที่ยังมี path (เดินขึ้น/ลงแล้วแอนิเมชันค้าง) */ + while (playPath.length > 0) { + const w2 = playPath[0]; + const ux = w2.x - me.x, uy = w2.y - me.y; + if (Math.sqrt(ux * ux + uy * uy) > PATH_ARRIVE_THRESH) break; + playPath.shift(); + } + if (playPath.length === 0) { + me.isWalking = false; + me.x = Math.max(0, Math.min(w - 0.01, me.x)); + me.y = Math.max(0, Math.min(h - 0.01, me.y)); + enforceQuizBattleLaneOnMePlay(); + const t = Date.now(); + if (t - lastSend > 80) { lastSend = t; socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } + draw(); + requestAnimationFrame(tick); + return; + } + usePath = true; + const next = playPath[0]; + accX = next.x - me.x; + accY = next.y - me.y; + } else { + usePath = true; + accX = dx; + accY = dy; + } + if (usePath && (accX !== 0 || accY !== 0)) { + if (Math.abs(accY) > Math.abs(accX)) me.direction = accY > 0 ? 'down' : 'up'; + else me.direction = accX > 0 ? 'right' : 'left'; + } + } + if (!usePath) { + let kx = 0; + let ky = 0; + if (keys['ArrowLeft'] || keys['KeyA']) kx -= 1; + if (keys['ArrowRight'] || keys['KeyD']) kx += 1; + if (keys['ArrowUp'] || keys['KeyW']) ky -= 1; + if (keys['ArrowDown'] || keys['KeyS']) ky += 1; + let jx = 0; + let jy = 0; + if (isQuizQuestionMissionHudActivePlay()) { + jx = quizQuestionMissionJoyVecX; + jy = quizQuestionMissionJoyVecY; + } + accX = kx + jx; + accY = ky + jy; + if (accX !== 0 || accY !== 0) { + if (Math.abs(accY) > Math.abs(accX)) me.direction = accY > 0 ? 'down' : 'up'; + else if (accX !== 0) me.direction = accX > 0 ? 'right' : 'left'; + } + } + const preWalkX = me.x, preWalkY = me.y; + if (accX !== 0 || accY !== 0) { + const len = Math.sqrt(accX * accX + accY * accY) || 1; + const step = Math.min(moveSpeedTilesThisFrameForWalk(), len); + const nx = me.x + (accX / len) * step; + const ny = me.y + (accY / len) * step; + /** Quiz Battle + เส้นทาง: ห้ามเลื่อนแกนเดียว (slide) — ไม่งั้นเดินทแยงหลุดออกนอกเลนได้ */ + const pathStrict = isQuizBattle() && quizBattlePathModeActive(mapData); + if (canWalkLikeLobby(nx, ny, me.x, me.y)) { + me.x = nx; + me.y = ny; + } else if (!pathStrict) { + if (canWalkLikeLobby(nx, me.y, me.x, me.y)) { + me.x = nx; + } else if (canWalkLikeLobby(me.x, ny, me.x, me.y)) { + me.y = ny; + } + } + } + me.x = Math.max(0, Math.min(w - 0.01, me.x)); + me.y = Math.max(0, Math.min(h - 0.01, me.y)); + enforceQuizBattleLaneOnMePlay(); + const movedThisTick = Math.abs(me.x - preWalkX) > 1e-5 || Math.abs(me.y - preWalkY) > 1e-5; + me.isWalking = !!(accX !== 0 || accY !== 0) || playPath.length > 0 || movedThisTick; + const now = Date.now(); + if (now - lastSend > 80) { lastSend = now; socket.emit('move', { x: me.x, y: me.y, direction: me.direction }); } + draw(); + requestAnimationFrame(tick); + } + + const chatForm = document.getElementById('chat-form'); + if (chatForm) { + chatForm.addEventListener('submit', (e) => { + e.preventDefault(); + const input = document.getElementById('chat-input'); + const text = (input && input.value || '').trim(); + if (text) { socket.emit('chat', text); if (input) input.value = ''; } + }); + } + document.getElementById('btn-leave').addEventListener('click', () => { + socket.emit('leave-space'); + const mid = params.get('map'); + if (previewMode && mid) { + var edQ = '?id=' + encodeURIComponent(mid) + (editorEmbedReturn ? '&embed=1' : ''); + window.location.replace(BASE + '/editor.html' + edQ); + } else { + window.location.replace(BASE + '/lobby.html'); + } + }); + (function wireQuizBattleMcqModal() { + const ov = document.getElementById('quiz-battle-mcq-overlay'); + if (!ov) return; + ov.addEventListener('click', (e) => { + const t = e.target; + if (!t) return; + if (t.classList && t.classList.contains('quiz-battle-choice')) { + const idx = parseInt(t.getAttribute('data-idx'), 10); + if (Number.isFinite(idx) && idx >= 0 && idx <= 2) trySubmitQuizBattleChoice(idx); + return; + } + if (t.id === 'quiz-battle-mcq-close') { + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + return; + } + if (t.classList && t.classList.contains('quiz-battle-mcq-backdrop')) { + quizBattleModalCompId = null; + hideQuizBattleMcqModal(); + } + }); + })(); + (function wireQuizCarryGrabButton() { + const btn = document.getElementById('quiz-carry-grab-btn'); + if (!btn) return; + btn.addEventListener('click', (ev) => { + if (!isQuizCarry() || !mapData || isChatFocused()) return; + if (!btn.classList.contains('quiz-carry-grab-btn--active')) return; + ev.preventDefault(); + if (myId == null) return; + tryQuizCarryInteractionForPlayer(myId, me.x, me.y, { fromKey: true }); + }); + })(); + + (function wireGauntletCrownJumpButton() { + const btn = document.getElementById('gauntlet-crown-jump-btn'); + if (!btn) return; + btn.addEventListener('click', (ev) => { + ev.preventDefault(); + if (!mapData || !isGauntlet() || isChatFocused()) return; + tryRequestGauntletJumpPlay(); + }); + })(); + + (function wireStackTowerDropButton() { + const btn = document.getElementById('stack-tower-drop-btn'); + if (!btn) return; + btn.addEventListener('click', (ev) => { + ev.preventDefault(); + if (!mapData || !isStack() || isChatFocused()) return; + if (!btn.classList.contains('stack-tower-drop-btn--active')) return; + tryHumanStackDrop(); + }); + })(); + + (function wireQuizQuestionMissionJoystick() { + const base = document.getElementById('quiz-q-mission-joystick-base'); + const bg = document.getElementById('quiz-q-mission-joystick-bg'); + const knobImg = document.getElementById('quiz-q-mission-joystick-knob-img'); + if (!base) return; + if (bg) { + bg.src = questionMissionAssetUrl('btn-joystick-1.png'); + bg.onerror = function () { + this.onerror = null; + this.removeAttribute('src'); + }; + } + if (knobImg) { + knobImg.src = questionMissionAssetUrl('btn-joystick-2.png'); + knobImg.onerror = function () { + this.onerror = null; + this.removeAttribute('src'); + }; + } + function endJoy(e) { + if (quizQuestionMissionJoyPointerId == null || e.pointerId !== quizQuestionMissionJoyPointerId) return; + try { + base.releasePointerCapture(e.pointerId); + } catch (_err) { /* ignore */ } + quizQuestionMissionJoyPointerId = null; + quizQuestionMissionJoystickResetVisual(); + } + base.addEventListener('pointerdown', (e) => { + if (!isQuizQuestionMissionHudActivePlay() || isChatFocused()) return; + if (e.button != null && e.button !== 0) return; + e.preventDefault(); + try { + base.setPointerCapture(e.pointerId); + } catch (_err) { /* ignore */ } + quizQuestionMissionJoyPointerId = e.pointerId; + quizQuestionMissionJoystickUpdateFromClientXY(e.clientX, e.clientY); + }); + base.addEventListener('pointermove', (e) => { + if (quizQuestionMissionJoyPointerId == null || e.pointerId !== quizQuestionMissionJoyPointerId) return; + e.preventDefault(); + quizQuestionMissionJoystickUpdateFromClientXY(e.clientX, e.clientY); + }); + base.addEventListener('pointerup', endJoy); + base.addEventListener('pointercancel', endJoy); + base.addEventListener('lostpointercapture', (e) => { + if (quizQuestionMissionJoyPointerId == null || e.pointerId !== quizQuestionMissionJoyPointerId) return; + quizQuestionMissionJoyPointerId = null; + quizQuestionMissionJoystickResetVisual(); + }); + })(); + + (function wireGauntletCrownHowtoPrimary() { + const btn = document.getElementById('btn-gch-ready'); + if (!btn) return; + btn.addEventListener('click', () => { + if (isJumpSurviveMissionUiMapPlay()) { + if (jumpSurviveMissionPhase !== 'howto') return; + const humans = quizCarryPregameHumanIds(); + const totPlayers = Math.max(1, quizCarryPregameTotalPlayers()); + if (totPlayers === 1) { + if (!(humans.length === 1 || isMePlayHost())) return; + beginJumpSurviveMissionCountdownThenRun(); + return; + } + if (!isMePlayHost()) { + if (myId == null) return; + const sid = String(myId); + const next = !gauntletCrownLobbyReadyMap[sid]; + gauntletCrownLobbyReadyMap[sid] = next; + updateJumpSurviveMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: next }); + return; + } + if (myId == null) return; + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + if (humansReady) { + socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (!r || !r.ok) { /* ignore */ } }); + return; + } + const sidH = String(myId); + const nextH = !gauntletCrownLobbyReadyMap[sidH]; + gauntletCrownLobbyReadyMap[sidH] = nextH; + updateJumpSurviveMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextH }); + return; + } + if (isSpaceShooterMissionUiMapPlay()) { + if (spaceShooterMissionPhase !== 'howto') return; + const humansSs = quizCarryPregameHumanIds(); + const totPlayersSs = Math.max(1, quizCarryPregameTotalPlayers()); + if (totPlayersSs === 1) { + if (!(humansSs.length === 1 || isMePlayHost())) return; + beginSpaceShooterMissionCountdownThenRun(); + return; + } + if (!isMePlayHost()) { + if (myId == null) return; + const sidSs = String(myId); + const nextSs = !gauntletCrownLobbyReadyMap[sidSs]; + gauntletCrownLobbyReadyMap[sidSs] = nextSs; + updateSpaceShooterMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextSs }); + return; + } + if (myId == null) return; + const humansReadySs = humansSs.length > 0 && humansSs.every((id) => !!gauntletCrownLobbyReadyMap[id]); + if (humansReadySs) { + socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (!r || !r.ok) { /* ignore */ } }); + return; + } + const sidHostSs = String(myId); + const nextHostSs = !gauntletCrownLobbyReadyMap[sidHostSs]; + gauntletCrownLobbyReadyMap[sidHostSs] = nextHostSs; + updateSpaceShooterMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextHostSs }); + return; + } + if (isQuizQuestionMissionUiMapPlay()) { + if (quizQuestionMissionPhase !== 'howto') return; + const humans = quizCarryPregameHumanIds(); + const totPlayers = Math.max(1, quizCarryPregameTotalPlayers()); + if (totPlayers === 1) { + beginQuizQuestionMissionCountdownThenRun(); + return; + } + if (!isMePlayHost()) { + if (myId == null) return; + const sid = String(myId); + const next = !gauntletCrownLobbyReadyMap[sid]; + gauntletCrownLobbyReadyMap[sid] = next; + updateQuizQuestionMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: next }); + return; + } + if (myId == null) return; + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + if (humansReady) { + socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (!r || !r.ok) { /* ignore */ } }); + return; + } + const sidH = String(myId); + const nextH = !gauntletCrownLobbyReadyMap[sidH]; + gauntletCrownLobbyReadyMap[sidH] = nextH; + updateQuizQuestionMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextH }); + return; + } + if (isStackTowerMissionUiMapPlay()) { + if (stackTowerMissionPhase !== 'howto') return; + const humansSt = quizCarryPregameHumanIds(); + const totPlayersSt = Math.max(1, quizCarryPregameTotalPlayers()); + if (totPlayersSt === 1) { + if (!(humansSt.length === 1 || isMePlayHost())) return; + beginStackTowerMissionCountdownThenRun(); + return; + } + if (!isMePlayHost()) { + if (myId == null) return; + const sidSt = String(myId); + const nextSt = !gauntletCrownLobbyReadyMap[sidSt]; + gauntletCrownLobbyReadyMap[sidSt] = nextSt; + updateStackTowerMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextSt }); + return; + } + if (myId == null) return; + const humansReadySt = humansSt.length > 0 && humansSt.every((id) => !!gauntletCrownLobbyReadyMap[id]); + if (humansReadySt) { + socket.emit('gauntlet-crown-lobby-start', {}, (r) => { if (!r || !r.ok) { /* ignore */ } }); + return; + } + const sidHostSt = String(myId); + const nextHostSt = !gauntletCrownLobbyReadyMap[sidHostSt]; + gauntletCrownLobbyReadyMap[sidHostSt] = nextHostSt; + updateStackTowerMissionHowtoHud(); + if (socket && socket.connected) socket.emit('gauntlet-crown-lobby-ready', { ready: nextHostSt }); + return; + } + if (!usesCrownLobbyShellPlay()) return; + if (gauntletCrownPregamePhase !== 'howto') return; + if (myId == null || !isMePlayHost()) return; + const humans = quizCarryPregameHumanIds(); + const humansReady = humans.length > 0 && humans.every((id) => !!gauntletCrownLobbyReadyMap[id]); + if (humansReady) { + if (humans.length === 1 && isMePlayHost()) { + beginGauntletCrownCountdownThenRun(); + return; + } + socket.emit('gauntlet-crown-lobby-start', {}, (r) => { + if (!r || !r.ok) { /* ignore */ } + }); + return; + } + const sid = String(myId); + const next = !gauntletCrownLobbyReadyMap[sid]; + gauntletCrownLobbyReadyMap[sid] = next; + updateGauntletCrownHowtoHud(); + socket.emit('gauntlet-crown-lobby-ready', { ready: next }); + }); + })(); + + (function wireQuizCarryPregameOverlay() { + const primary = document.getElementById('quiz-carry-pregame-primary'); + if (!primary) return; + primary.addEventListener('click', () => { + if (!quizCarryPregameActive || myId == null) return; + if (!isMePlayHost()) return; + const humans = quizCarryPregameHumanIds(); + const humansReady = humans.length > 0 && humans.every((id) => !!quizCarryLobbyReadyMap[id]); + if (humansReady) { + /* ผู้เล่นจริงคนเดียว + พรีวิว: ไม่ต้องรอ ACK เซิร์ฟ (ห้องที่ map บนเซิร์ฟยังไม่ใช่ quiz_carry จะเคยล้มเหลว) */ + if (humans.length === 1 && isMePlayHost()) { + endQuizCarryEmbedPregameAndStart(); + return; + } + socket.emit('quiz-carry-lobby-start', {}, (r) => { + if (!r || !r.ok) showQuizCarryToast((r && r.error) || 'START ไม่ได้', false); + }); + return; + } + const sid = String(myId); + const next = !quizCarryLobbyReadyMap[sid]; + quizCarryLobbyReadyMap[sid] = next; + updateQuizCarryPregameHud(); + socket.emit('quiz-carry-lobby-ready', { ready: next }); + }); + })(); + window.addEventListener('beforeunload', () => socket.emit('leave-space')); + window.addEventListener('resize', () => { + if (!mapData || !canvas) return; + resizeCanvas(); + draw(); + }); + /** แคนวาส flex ขยายทีหลัง (จอใหญ่ / editor iframe) — ResizeObserver จับได้ดีกว่า resize อย่างเดียว */ + (function setupPlayCanvasStackResizeObserver() { + const stack = document.getElementById('play-canvas-stack'); + if (!stack || typeof ResizeObserver === 'undefined') return; + let roRaf = null; + const run = () => { + roRaf = null; + if (!mapData || !canvas) return; + resizeCanvas(); + draw(); + }; + const ro = new ResizeObserver(() => { + if (roRaf) cancelAnimationFrame(roRaf); + roRaf = requestAnimationFrame(run); + }); + ro.observe(stack); + const wrap = stack.closest('.game-wrap'); + if (wrap) ro.observe(wrap); + const stageEl = document.getElementById('play-canvas-stage'); + if (stageEl) ro.observe(stageEl); + })(); +})(); diff --git a/www/html/Login/login.js b/www/html/Login/login.js index 90bc380..e623477 100644 --- a/www/html/Login/login.js +++ b/www/html/Login/login.js @@ -5,7 +5,7 @@ var linkPrivacy = document.getElementById('link-privacy'); function loadOauthPublic() { - return fetch('/Admin/api/oauth-public.php', { credentials: 'omit' }) + return fetch((typeof appPath === 'function' ? appPath('/Admin/api/oauth-public.php') : '/Admin/api/oauth-public.php'), { credentials: 'omit' }) .then(function (r) { return r.json(); }) .then(function (j) { if (!j || !j.ok) return null; @@ -18,7 +18,7 @@ // ไปหน้า Main Lobby แบบ Guest localStorage.setItem('isLoggedIn', 'true'); localStorage.setItem('loginType', 'guest'); - window.location.href = '/Main-Lobby/'; + window.location.href = typeof appPath === 'function' ? appPath('/Main-Lobby/') : '/Main-Lobby/'; } if (btnFacebook) { @@ -28,7 +28,7 @@ alert('ยังไม่ได้ตั้งค่า Facebook App ในหน้า /Admin/'); return; } - var ru = cfg.facebookRedirectUri || (location.origin + '/Login/facebook-callback.html'); + var ru = cfg.facebookRedirectUri || (location.origin + (typeof appPath === 'function' ? appPath('/Login/facebook-callback.html') : '/Login/facebook-callback.html')); var state = encodeURIComponent('fb-' + Date.now()); var url = 'https://www.facebook.com/v18.0/dialog/oauth?client_id=' + encodeURIComponent(cfg.facebookAppId) + '&redirect_uri=' + encodeURIComponent(ru) + '&state=' + state + '&scope=email,public_profile'; @@ -44,7 +44,7 @@ alert('ยังไม่ได้ตั้งค่า Google Client ในหน้า /Admin/'); return; } - var ru = cfg.googleRedirectUri || (location.origin + '/Login/google-callback.html'); + var ru = cfg.googleRedirectUri || (location.origin + (typeof appPath === 'function' ? appPath('/Login/google-callback.html') : '/Login/google-callback.html')); var state = encodeURIComponent('go-' + Date.now()); var url = 'https://accounts.google.com/o/oauth2/v2/auth?client_id=' + encodeURIComponent(cfg.googleClientId) + '&redirect_uri=' + encodeURIComponent(ru) + '&response_type=code&scope=' +

%q|;lmNPZa&D1<6Q%uejo7;elwV;G8 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 E0X>K<(f|Me literal 0 HcmV?d00001 diff --git a/www/html/Create Room/FONTS/NotoSansThai-VariableFont_wdth,wght.ttf b/www/html/Create Room/FONTS/NotoSansThai-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000000000000000000000000000000000000..99a7ec710b840563e0fb7aa0ae0c22f665e80788 GIT binary patch literal 217004 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 q && q.id === myId)); diff --git a/www/html/Game/public/js/play.js.bak-20260526-151951 b/www/html/Game/public/js/play.js.bak-20260526-151951 new file mode 100644 index 0000000..890bf52 --- /dev/null +++ b/www/html/Game/public/js/play.js.bak-20260526-151951 @@ -0,0 +1,18704 @@ +(function () { + const BASE = '/Game'; + /** รูปเลข 3–2–1 — เสิร์ฟที่ /Game/img/QUESTION/ (nginx alias /Game/ → public; อย่าใช้ /Game/public/img/) */ + const COUNTDOWN_321_IMG_BASE = BASE + '/img/QUESTION/'; + /** แผงคำถามบนแมป quiz_carry — ไฟล์อยู่ public/img/quiz-carry */ + const QUIZ_CARRY_MAP_FEEDBACK_IMG = { + correct: BASE + '/img/quiz-carry/icon-Correct.png', + incorrect: BASE + '/img/quiz-carry/icon-Incorrect.png', + scorePlus: BASE + '/img/quiz-carry/score+10.png', + }; + const QUIZ_MAP_CARRY_FEEDBACK_MS = 1400; + const params = new URLSearchParams(window.location.search); + const spaceId = params.get('space'); + const nick = params.get('nick') || 'ผู้เล่น'; + const previewMode = params.get('preview') === '1'; + const forceDefaultCharacter = params.get('defaultChar') === '1'; + const editorEmbedReturn = params.get('editorEmbed') === '1'; + if (previewMode && editorEmbedReturn) { + try { document.documentElement.classList.add('play-preview-editor-embed'); } catch (e) { /* ignore */ } + } + const playMapIdFromQuery = (params.get('map') || '').trim(); + /** mapId จาก join / game-start — ใช้จับฉาก crown เมื่อ URL ไม่มี ?map= */ + let playSessionMapId = playMapIdFromQuery; + /** สรุปภารกิจแบบ mock (popup-result ฯลฯ) — ใช้เฉพาะฉากนี้จาก editor (id ใน URL เช่น editor.html?id=mnorwqx1) */ + const quizCarryMissionSummaryMapId = 'mnorwqx1'; + const quizCarryUseMissionSummaryOverlay = playMapIdFromQuery === quizCarryMissionSummaryMapId; + /** Gauntlet พรมแดง — ฉากนี้จาก editor (?map=mno9kb07): วาดตัวหันขวา + ใช้รูป lane/laser จาก game-timing */ + const GAUNTLET_FACE_RIGHT_MAP_ID = 'mno9kb07'; + /** Jump Survival ฉาก Jumper — UI สรุปภารกิจแบบ crown + รูปใน /img/Jumper/ (editor ?map=mnptfts2) */ + const JUMP_SURVIVE_MISSION_MAP_ID = 'mnptfts2'; + /** Space shooter ฉาก Violent Crime — flow เดียวกับ crown/howto (รูปใน /img/ViolentCrime/) */ + const SPACE_SHOOTER_MISSION_MAP_ID = 'mnpz6rkp'; + /** Quiz ฉากสอบสวน — flow เดียวกับ crown/howto แต่รูปใน /img/QUESTION/ */ + const QUIZ_QUESTION_MISSION_MAP_ID = 'mng8a80o'; + /** Stack ซ้อนตึก — ฉากภารกิจ (HOWTO → นับถอยหลัง → เล่น → สรุป) รูปใน `/Game/img/TowerBlock` */ + const STACK_TOWER_MISSION_MAP_ID = 'mnn93hpi'; + /** Stack Tower: บัฟเฟอร์วาดคงที่ 16:9 — 1920×1080 */ + const STACK_TOWER_FIXED_RENDER_W = 1920; + const STACK_TOWER_FIXED_RENDER_H = 1080; + /** คูณ zDraw ให้มุมมองโลกเท่าเดิมเมื่อบัฟเฟอร์กว้างกว่าฐานอ้างอิง 1280w (= 1.5 ที่ 1920w) */ + const STACK_TOWER_FIXED_RENDER_Z_MUL = STACK_TOWER_FIXED_RENDER_W / 1280; + /** เชือก Stack Tower: ปลายล่างตามเส้น (1 = ถึงจุดยึดบล็อก) */ + const STACK_TOWER_ROPE_DRAW_T1 = 1.0; + /** ปลายบนเชือก (จอ): ยืดเหนือขอบบนแคนวาสเป็นเศษส่วนของความสูง (เช่น 0.25 = 25% นอกจอด้านบน) */ + const STACK_TOWER_ROPE_TOP_ABOVE_CANVAS_FRAC = 0.25; + /** progress ≥ นี้ (ฉาก Tower mnn93hpi): รูป heavy + ความกว้าง heavy = เท่าเดียวกับปกติ × ค่าด้านล่าง + แอนิเมตซูม/เลื่อน */ + const STACK_TOWER_POST50_PROGRESS_THRESH = 60; + const STACK_TOWER_POST50_ZOOM_MUL = 1.1; + /** เลื่อน BG แนวตั้งเท่าเศษส่วนของความสูงแคนวาส (จอ) */ + const STACK_TOWER_POST50_MAP_SHIFT_SCREEN_FRAC = 0.1; + const STACK_TOWER_POST50_ANIM_MS = 680; + /** แมป mnn93hpi: สเกลความกว้าง/ความสูงชั้นของบล็อกในโลก (1 = ขนาดเดิม) */ + const STACK_TOWER_BLOCK_WORLD_SCALE = 1.5; + /** หลังเกณฑ์ progress (Tower): เลื่อนกล้องลงในมุมมองจอเท่าเศษส่วนของความสูงแคนวาส (เช่น 0.25 = 25% ของความสูงจอ) */ + const STACK_TOWER_POST50_VIEW_SHIFT_SCREEN_FRAC = 0.25; + /** Mega Virus — balloon_boss ฉากภารกิจ (flow เดียวกับ crown / mno9kb07) รูปใน `/Game/img/MegaVirus` */ + const BALLOON_BOSS_MISSION_MAP_ID = 'mnq1eml7'; + /** ครบชั้นนี้ขึ้นไป — ยกจุดแกว่ง/เชือกขึ้น (world px) ป้องกันชนกองสูง (เฉพาะ Tower mission) */ + const STACK_TOWER_SWING_LIFT_FROM_LAYER = 9; + /** Stack — เกณฑ์ใกล้ความจริง: ทับมากขึ้น, จำกัดการเยื้องจากกลางฐาน, ทับน้อยแล้วโคลงง่ายขึ้น */ + const STACK_OVERLAP_MISS_MIN_ON_LAND = 0.42; + const STACK_OVERLAP_MISS_FRAC_ON_LAND = 0.14; + const STACK_OVERLAP_MISS_MIN_ON_LAYER = 0.4; + const STACK_OVERLAP_MISS_FRAC_ON_LAYER = 0.3; + /** จุดกลางแท่งที่วาง vs กลางโซน land — เกิน (ความกว้างโซน × นี้) = พลาด (ชั้นที่ 2 ขึ้นไป) */ + const STACK_MAX_CENTER_DRIFT_FRAC_OF_LAND = 0.38; + /** ไม่มี stackLandArea: ใช้กลางแมป — เยื้องได้ไม่เกิน fallW × นี้ (รวม) */ + const STACK_MAX_CENTER_DRIFT_NO_LAND_MULT = 1.0; + /** overlap/fallW ต่ำกว่านี้ → เข้า settling (แท่งไม่นิ่ง) */ + const STACK_STABLE_SUPPORT_RATIO_MIN = 0.62; + /** Stack ทั่วไป (ไม่ใช่ Tower post-50%): บล็อก heavy กว้างกว่าปกติ (~382×72 vs 314×103) */ + const STACK_BLOCK_HEAVY_WIDTH_MULT = 382 / 314; + /** Tower live + progress ≥เกณฑ์ + heavy: ความกว้างบล็อก = ปกติ × ค่านี้ (เช่น 0.6 = 60% ของช่องปกติ — รูปใหญ่วาดในกรอบเดียวกัน) */ + const STACK_TOWER_POST50_WIDTH_MULT = 0.6; + + function stackBlockWidthTilesForPlay(baseTiles, heavy) { + const b = Number(baseTiles); + const base = Number.isFinite(b) ? Math.max(0.85, Math.min(3.2, b)) : 2; + const towerHeavyNarrow = + !!heavy && + isStackTowerMissionUiMapPlay() && + stackTowerMissionPhase === 'live' && + stackMini && + Number.isFinite(Number(stackMini.progressPct)) && + Number(stackMini.progressPct) >= STACK_TOWER_POST50_PROGRESS_THRESH; + if (towerHeavyNarrow) { + const w = base * STACK_TOWER_POST50_WIDTH_MULT; + return Math.max(0.85, Math.min(3.2, Math.round(w * 200) / 200)); + } + if (!heavy) return base; + const w = base * STACK_BLOCK_HEAVY_WIDTH_MULT; + return Math.min(3.2, Math.round(w * 200) / 200); + } + /** Violent Crime mission: asteroid strikes ship → invuln → 3 strikes = eliminated */ + const SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS = 3; + const SPACE_SHOOTER_MISSION_HIT_INVULN_MS = 1400; + const SPACE_SHOOTER_MISSION_SHIP_HIT_RADIUS = 12; + /** อุกาบาต: HP สุ่ม 1–5 ต่อก้อน (ไม่วาดหัวใจบนก้อน) — ขนาดตาม HP เทียบสเกล 5 */ + const SPACE_SHOOTER_ASTEROID_MAX_HP = 5; + const SPACE_SHOOTER_ASTEROID_RADIUS_AT_FULL = 48; + const SPACE_SHOOTER_ASTEROID_RADIUS_AT_MIN_HP = 11; + + function spaceShooterAsteroidRadiusFromHpPlay(hp) { + const cap = SPACE_SHOOTER_ASTEROID_MAX_HP; + const h = Math.max(0, Math.min(cap, Math.floor(Number(hp)) || 0)); + const frac = h <= 0 ? 0 : h / cap; + return SPACE_SHOOTER_ASTEROID_RADIUS_AT_MIN_HP + + (SPACE_SHOOTER_ASTEROID_RADIUS_AT_FULL - SPACE_SHOOTER_ASTEROID_RADIUS_AT_MIN_HP) * frac; + } + + function spaceShooterShipFootprintCellsPlay(md) { + const m = md || mapData; + if (!m) return { cw: 1, ch: 1 }; + const { cw, ch } = getCharacterFootprintWH(m); + return { + cw: Math.max(1, Math.min(4, cw)), + ch: Math.max(1, Math.min(4, ch)), + }; + } + + /** ขนาดวาดยานบนจอ — สเกลตาม footprint แผนที่ (editor: กว้าง/สูงตัวละคร) เทียบ tileSize */ + function spaceShooterShipBodyScreenPxPlay(zDrawVal) { + const zD = Number(zDrawVal) > 0 ? zDrawVal : (typeof zoom === 'number' && zoom > 0 ? zoom : 1); + const ts = tileSize || 32; + const { cw, ch } = spaceShooterShipFootprintCellsPlay(mapData); + const kw = 14 / 32; + const kh = 22 / 32; + return { + bodyW: cw * ts * zD * kw, + bodyH: ch * ts * zD * kh, + }; + } + + /** รัศมีชนอุกาบาต (พิกัดโลก) — โตตาม footprint เพื่อให้สมกับยานที่วาดใหญ่ขึ้น */ + function spaceShooterShipHitRadiusWorldPxPlay() { + if (!mapData || mapData.gameType !== 'space_shooter') return SPACE_SHOOTER_MISSION_SHIP_HIT_RADIUS; + const ts = tileSize || 32; + const { cw, ch } = spaceShooterShipFootprintCellsPlay(mapData); + const base = SPACE_SHOOTER_MISSION_SHIP_HIT_RADIUS; + const geom = Math.sqrt(Math.max(1, cw * ch)); + const cap = Math.min(54, 0.42 * Math.min(cw * ts, ch * ts)); + return Math.max(9, Math.min(cap, base * geom)); + } + + /** ระยะห่างจากขอบโลกซ้าย/ขวาเมื่อบังคับยาน — กันยานใหญ่หลุดกรอบ */ + function spaceShooterShipEdgePadWorldPxPlay() { + if (!mapData || mapData.gameType !== 'space_shooter') return 18; + const ts = tileSize || 32; + const { cw } = spaceShooterShipFootprintCellsPlay(mapData); + return Math.max(18, cw * ts * 0.48); + } + + function spaceShooterResetMissionShipStateForAllPlay() { + function resetEnt(ent) { + if (!ent) return; + ent.spaceShooterHits = 0; + ent.spaceShooterEliminated = false; + ent.spaceShooterInvulnUntil = 0; + } + resetEnt(me); + others.forEach(function (o) { + resetEnt(o); + }); + } + + function spaceShooterMissionResolveShipAsteroidHitsPlay() { + if (!mapData || mapData.gameType !== 'space_shooter') return; + if (!isSpaceShooterMissionUiMapPlay() || spaceShooterMissionPhase !== 'live' || spaceShooterGameEnded) return; + const now = performance.now(); + const shipR = spaceShooterShipHitRadiusWorldPxPlay(); + + function tryHit(ent, cx, cy) { + if (!ent || ent.spaceShooterEliminated || cx == null || cy == null) return; + if (ent.spaceShooterInvulnUntil != null && now < ent.spaceShooterInvulnUntil) return; + for (let ai = spaceShooterAsteroids.length - 1; ai >= 0; ai--) { + const a = spaceShooterAsteroids[ai]; + if (!a) continue; + const dx = cx - a.x; + const dy = cy - a.y; + const rr = (a.r + shipR) * (a.r + shipR); + if (dx * dx + dy * dy > rr) continue; + ent.spaceShooterHits = Math.max(0, Number(ent.spaceShooterHits) || 0) + 1; + ent.spaceShooterInvulnUntil = now + SPACE_SHOOTER_MISSION_HIT_INVULN_MS; + spaceShooterSpawnAsteroidExplosion(a.x, a.y, a.r); + spaceShooterAsteroids.splice(ai, 1); + spaceShooterPopups.push({ x: cx, y: cy - 28, text: 'HIT', until: Date.now() + 650 }); + if (ent.spaceShooterHits >= SPACE_SHOOTER_MISSION_MAX_ASTEROID_HITS) { + ent.spaceShooterEliminated = true; + spaceShooterPopups.push({ x: cx, y: cy - 50, text: 'OUT', until: Date.now() + 1400 }); + } + break; + } + } + + if (myId != null) tryHit(me, me.spaceShooterCx, me.spaceShooterCy); + others.forEach(function (o) { + if (!o) return; + tryHit(o, o.spaceShooterCx, o.spaceShooterCy); + }); + if (spaceShooterMissionEveryParticipantEliminatedPlay()) { + endSpaceShooterMissionRound('all_dead'); + } + } + + /** Space Shooter ภารกิจ: ผู้เข้าร่วมทุกคนถูกกำจัดแล้ว (รวมเรา) */ + function spaceShooterMissionEveryParticipantEliminatedPlay() { + if (!isSpaceShooterMissionUiMapPlay() || spaceShooterMissionPhase !== 'live' || spaceShooterGameEnded) return false; + if (myId == null) return false; + if (!me.spaceShooterEliminated) return false; + let anyAlive = false; + others.forEach(function (o) { + if (!o) return; + if (!o.spaceShooterEliminated) anyAlive = true; + }); + return !anyAlive; + } + + /** จนกว่าโหลด /api/characters — placeholder ชั่วคราวก่อน join */ + const LEGACY_PLACEHOLDER_CHARACTER_ID = 'Chatest'; + let firstCharacterDefaultResolved = null; + /** รหัสตัวละครจาก GET /api/characters — ใช้สลับบอทพรีวิวให้ไม่ซ้ำรูปกับผู้เล่น */ + let playCharacterIdRoster = []; + const firstCharacterDefaultPromise = fetch(BASE + '/api/characters') + .then((r) => r.json()) + .then((list) => { + playCharacterIdRoster = []; + if (Array.isArray(list)) { + list.forEach((c) => { + if (c && c.id) playCharacterIdRoster.push(String(c.id)); + }); + } + if (Array.isArray(list) && list.length > 0 && list[0] && list[0].id) { + firstCharacterDefaultResolved = String(list[0].id); + } else { + firstCharacterDefaultResolved = ''; + } + }) + .catch(() => { + firstCharacterDefaultResolved = ''; + playCharacterIdRoster = []; + }) + .finally(() => { + try { + if (playBotsEnabled() && mapData) rebalancePreviewBots(); + } catch (e) { /* map ยังไม่พร้อม */ } + }); + const lobbyLevelParam = params.get('lobbyLevel'); + const lobbyCaseParam = params.get('case'); + if (lobbyLevelParam || lobbyCaseParam) { + try { + window.__detectiveLobbyMeta = { level: lobbyLevelParam, caseId: lobbyCaseParam }; + } catch (e) { /* ignore */ } + } + + const POST_CASE_LOBBY_MAP_ID = 'mn8nx46h'; + + function isDetectiveMinigamePlay() { + if (params.get('detectiveReturn') === '1') return true; + try { + return sessionStorage.getItem('detectiveMinigameReturn') === '1'; + } catch (e) { + return false; + } + } + + function buildRoomLobbyReturnHref() { + let h = 'room-lobby.html?space=' + encodeURIComponent(spaceId) + '&nick=' + encodeURIComponent(nick); + h += '&map=' + encodeURIComponent(POST_CASE_LOBBY_MAP_ID); + const drParam = (params.get('displayRoom') || '').trim(); + if (drParam) { + h += '&displayRoom=' + encodeURIComponent(drParam); + } else { + try { + const dr = localStorage.getItem('lastCreatedSpaceName'); + if (dr) h += '&displayRoom=' + encodeURIComponent(dr); + } catch (e) { /* ignore */ } + } + try { + const meta = window.__detectiveLobbyMeta; + if (meta && meta.level != null && String(meta.level).trim()) { + h += '&lobbyLevel=' + encodeURIComponent(String(meta.level).trim()); + } + if (meta && meta.caseId != null && String(meta.caseId).trim()) { + h += '&caseId=' + encodeURIComponent(String(meta.caseId).trim()); + } + } catch (e2) { /* ignore */ } + try { + if (sessionStorage.getItem('detectiveMinigameReturn') === '1') { + h += '&detectiveReturn=1'; + sessionStorage.removeItem('detectiveMinigameReturn'); + } + } catch (e3) { /* ignore */ } + if (params.get('detectiveReturn') === '1' && h.indexOf('detectiveReturn') < 0) { + h += '&detectiveReturn=1'; + } + return h; + } + + function finishDetectiveMinigameAndReturnLobby() { + quizCarrySessionEnded = true; + cancelQuizCarryAnswerRoundTimeupAuto(); + quizCarryAnswerTimeupAwaitNext = false; + hideQuizCarryTimeupOnDeskLayer(); + try { sessionStorage.setItem('detectiveMinigameReturn', '1'); } catch (e) { /* ignore */ } + const go = function () { + window.location.href = buildRoomLobbyReturnHref(); + }; + if (socket && socket.connected) { + socket.emit('detective-minigame-finished', {}, function (res) { + go(); + }); + } else { + go(); + } + } + + /** จบมินิเกมโหมดสืบสวน → LobbyB; คืน true ถ้า redirect แล้ว */ + function tryFinishDetectiveMinigameAndReturnLobby() { + if (!isDetectiveMinigamePlay()) return false; + finishDetectiveMinigameAndReturnLobby(); + return true; + } + + /** ทดสอบจากเอดิเตอร์: เติมบอทให้ครบจำนวน (รวมผู้เล่นจริง) — ?fillTotal=6 (ค่าเริ่ม 6) */ + const previewFillBots = previewMode && editorEmbedReturn; + let detectiveCaseFillBots = false; + let detectiveBotSlotCount = 0; + function playBotsEnabled() { + return previewFillBots || detectiveCaseFillBots; + } + function playBotTargetHeadcount() { + if (previewFillBots) return previewTargetHeadcount; + if (detectiveCaseFillBots) return countPlayHumans() + detectiveBotSlotCount; + return countPlayHumans(); + } + /** พรีวิวใน iframe เอดิเตอร์: ซูมมุมมองได้ (เดิมไม่มี — zDraw จาก zoom คงที่) */ + const PLAY_EMBED_USER_ZOOM_MIN = 0.5; + const PLAY_EMBED_USER_ZOOM_MAX = 3; + const PLAY_EMBED_ZOOM_STEP_KEY = 1.12; + let playEmbedUserZoomMul = 1; + let lastPlayZDrawForInput = 1.4; + /** คูณ world→พิกเซลบนแคนวาสให้ตรงกับ zDraw ใน draw() (รวม preview+embed zoom) — ใช้ซิงก์ DOM ทับฉาก */ + function playDomSyncZoom() { + const z = lastPlayZDrawForInput; + return Number.isFinite(z) && z > 0 ? z : zoom; + } + const previewTargetHeadcount = Math.min(24, Math.max(1, parseInt(params.get('fillTotal'), 10) || 6)); + const PREVIEW_BOT_PREFIX = '__pv_bot_'; + let previewBotSeq = 0; + /** Stack preview: บอทไม่วาบบนแผนที่ — ใช้ไฮไลต์แถบแข่งแทน */ + let lastStackPreviewActorId = null; + let lastStackPreviewActorUntil = 0; + /** Stack preview: สลับตา Player 1 = มนุษย์, 2–6 = บอท (เฉพาะตาปัจจุบันเล่นได้) */ + const STACK_PREVIEW_TURN_COUNT = 6; + let stackPreviewTurnOrder = null; + let stackPreviewTurnIndex = 0; + let stackPreviewBotThinkUntil = 0; + + function rebuildStackPreviewTurnOrder() { + if (!playBotsEnabled() || !mapData || mapData.gameType !== 'stack') { + stackPreviewTurnOrder = null; + return; + } + const bots = [...others.keys()].filter(isPreviewBotId).sort(); + stackPreviewTurnOrder = [{ kind: 'human', seat: 1 }]; + for (let i = 0; i < STACK_PREVIEW_TURN_COUNT - 1; i++) { + stackPreviewTurnOrder.push({ kind: 'bot', seat: i + 2, botId: bots[i] || null }); + } + stackPreviewTurnIndex = 0; + stackPreviewBotThinkUntil = 0; + markStackNextDropVisualDirty(); + } + + function advanceStackPreviewTurn() { + if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return; + stackPreviewTurnIndex = (stackPreviewTurnIndex + 1) % STACK_PREVIEW_TURN_COUNT; + stackPreviewBotThinkUntil = 0; + markStackNextDropVisualDirty(); + } + + function isStackPreviewHumanTurn() { + if (!playBotsEnabled() || !stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return true; + const cur = stackPreviewTurnOrder[stackPreviewTurnIndex]; + return !!(cur && cur.kind === 'human'); + } + + function getStackPreviewCurrentSeat() { + if (!stackPreviewTurnOrder || stackPreviewTurnOrder.length !== STACK_PREVIEW_TURN_COUNT) return 1; + const cur = stackPreviewTurnOrder[stackPreviewTurnIndex]; + return cur && cur.seat ? cur.seat : stackPreviewTurnIndex + 1; + } + + function getStackDropActorSeat() { + if (playBotsEnabled() && stackPreviewTurnOrder && stackPreviewTurnOrder.length === STACK_PREVIEW_TURN_COUNT) { + return getStackPreviewCurrentSeat(); + } + return 1; + } + + function markStackNextDropVisualDirty() { + if (stackMini) stackMini.nextDropVisualDirty = true; + } + + function pickStackBlockHeavyVisual(seat) { + const slot = Math.max(0, Math.min(5, (Math.floor(Number(seat)) || 1) - 1)); + const hU = normalizeGauntletAssetUrlForPlay(playStackBlockHeavyUrls[slot] || ''); + /** Tower mnn93hpi: ตั้งแต่เกณฑ์ progress (ตาม stackTowerProgressBlocks) → ใช้รูป heavy จาก admin — นอก live ไม่สุ่ม heavy */ + if (isStackTowerMissionUiMapPlay()) { + if (!(stackTowerMissionPhase === 'live' && stackMini)) return false; + const p = Number(stackMini.progressPct); + if (!Number.isFinite(p) || p < STACK_TOWER_POST50_PROGRESS_THRESH) return false; + return !!hU; + } + const pct = Math.max(0, Math.min(100, Math.floor(Number(playStackHeavyBlockPercent) || 0))); + if (pct <= 0) return false; + if (!hU) return false; + if (Math.random() * 100 >= pct) return false; + return true; + } + + function ensureStackNextDropVisual() { + if (!stackMini || !stackMini.nextDropVisualDirty) return; + stackMini.pendingDropSeat = getStackDropActorSeat(); + stackMini.pendingDropHeavy = pickStackBlockHeavyVisual(stackMini.pendingDropSeat); + stackMini.widthTiles = stackBlockWidthTilesForPlay(stackMini.initialWidthTiles, !!stackMini.pendingDropHeavy); + stackMini.nextDropVisualDirty = false; + } + + function normalizePlayStackSixUrlsFromTiming(arr) { + const out = ['', '', '', '', '', '']; + if (!Array.isArray(arr)) return out; + for (let i = 0; i < 6; i++) { + out[i] = normalizeGauntletAssetUrlForPlay(typeof arr[i] === 'string' ? arr[i] : ''); + if (out[i]) ensureGauntletAssetImage(out[i]); + } + return out; + } + + function resolveStackBlockSpriteRec(seat, heavy) { + const slot = Math.max(0, Math.min(5, (Math.floor(Number(seat)) || 1) - 1)); + const tryHeavy = !!heavy; + const order = tryHeavy + ? [playStackBlockHeavyUrls[slot], playStackBlockNormalUrls[slot]] + : [playStackBlockNormalUrls[slot], playStackBlockHeavyUrls[slot]]; + for (let ti = 0; ti < order.length; ti++) { + const u = normalizeGauntletAssetUrlForPlay(order[ti] || ''); + if (!u) continue; + return ensureGauntletAssetImage(u); + } + return null; + } + + /** + * @returns {boolean} true = วาดสไปรต์แล้ว · false = วาดสี่เหลี่ยมสี fallback + * ใช้ scale แบบ cover + clip ให้ขอบภาพตรงกล่องฟิสิกส์ (drawW×drawH) — ไม่เว้นขอบซ้ายขวาแบบ contain + * เพื่อให้สายตาตรงกับ overlap / miss ใน computeStackDropResult + */ + function drawStackBlockSpriteOrHue(ctx, sx, sy, drawW, drawH, seat, heavy, hueFallback, opts) { + const o = opts || {}; + const rec = resolveStackBlockSpriteRec(seat, heavy); + const img = rec && rec.img; + if (rec && rec.ready && img && img.complete && img.naturalWidth > 0 && img.naturalHeight > 0) { + const iw = img.naturalWidth; + const ih = img.naturalHeight; + if (drawW > 0 && drawH > 0) { + const scale = Math.max(drawW / iw, drawH / ih); + const dw = iw * scale; + const dh = ih * scale; + const dx = sx + (drawW - dw) * 0.5; + const dy = sy + (drawH - dh) * 0.5; + const prevSmooth = ctx.imageSmoothingEnabled; + ctx.save(); + ctx.beginPath(); + ctx.rect(sx, sy, drawW, drawH); + ctx.clip(); + try { + ctx.imageSmoothingEnabled = false; + ctx.drawImage(img, 0, 0, iw, ih, dx, dy, dw, dh); + } finally { + ctx.imageSmoothingEnabled = prevSmooth; + } + ctx.restore(); + if (o.missOverlay) { + ctx.fillStyle = 'rgba(247, 118, 190, 0.42)'; + ctx.fillRect(sx, sy, drawW, drawH); + } + return true; + } + } + const hue = ((hueFallback % 360) + 360) % 360; + ctx.fillStyle = o.missTint ? 'rgba(247, 118, 190, 0.88)' : ('hsla(' + hue + ', 68%, 56%, 0.9)'); + ctx.fillRect(sx, sy, drawW, drawH); + return false; + } + + if (!spaceId) { window.location.replace(BASE + '/lobby.html'); return; } + + const socket = io({ path: BASE + '/socket.io' }); + const canvas = document.getElementById('game-canvas'); + const ctx = canvas.getContext('2d'); + let mapData = null, tileSize = 32, myId = null; + let stackMini = null; + let stackFall = null; + /** Tower / Stack — เชือกเหวี่ยงจาก `img/TowerBlock/sling.png` */ + let stackTowerSlingImg = null; + /** Tower mnn93hpi — +คะแนน (`score+10.png` / `score+20.png`) + เอฟเฟกต์ ×2 (`score-light.png`, `scorex2.png`) */ + let stackTowerScorePlus10Img = null; + let stackTowerScorePlus20Img = null; + let stackTowerScoreLightImg = null; + let stackTowerScoreX2Img = null; + /** พลาดแล้วโชว์ heart-1.png กลางเหนือกอง (mock Tower) — heart เต็มใน HUD ใช้ไฟล์เดียวกัน */ + let stackTowerHeartMinusImg = null; + /** HUD มุมขวา: `life-bar.png` + `life-full.png` / `life-hit.png` (mock รูป2) */ + let stackTowerLifeBarImg = null; + let stackTowerLifeHitImg = null; + let stackTowerLifeFullImg = null; + /** @type {{ until: number } | null} */ + let stackTowerHeartMinusFx = null; + const STACK_TOWER_HEART_MINUS_FX_MS = 1500; + const STACK_TOWER_PERFECT_GLOW_MS = 1500; + /** ป๊อปอัป +10 / +20 เหนือบล็อก */ + const STACK_TOWER_SCORE_POPUP_MS = 2000; + /** ระยะโชว์ score-light + scorex2 ข้างชั้นบน (คู่กับ +20 / perfect full) */ + const STACK_TOWER_PERFECT_X2_MS = 2200; + /** ทับบล็อกก่อนหน้า + ชั้นใหม่: support เต็มแทบไม่มีเพี้ยน */ + const STACK_TOWER_PERFECT_SUPPORT_MIN = 0.998; + let lastStackTickMs = performance.now(); + let lastStackTowerScrollBgTickMs = performance.now(); + let mapBackgroundImg = null; + /** space_shooter + แมป mnpz6rkp — พื้นหลังเลื่อนแนวตั้ง (ข้อมูลจาก editorBgScroll ในแมป) */ + const PLAY_SCROLL_BG_MAP_ID = 'mnpz6rkp'; + const PLAY_SCROLL_BG_DEFAULT_INTRO = BASE + '/img/editor-bg-mnpz6rkp/intro.png'; + const PLAY_SCROLL_BG_DEFAULT_LOOP = BASE + '/img/editor-bg-mnpz6rkp/loop.png'; + let playScrollBgIntroImg = null; + let playScrollBgLoopImg = null; + /** ขอบบนของ viewport ใน strip — เพิ่มค่า = เลื่อนลง strip (เข้า loop ใต้ intro) */ + let playScrollBgPx = 0; + let playScrollBgSpeedPxPerSec = 56; + let playScrollBgOff = false; + /** true = บน→ล่าง: พลิกมุมมองใน viewport (ยังเลื่อน S+ เข้า loop ใต้ intro) */ + let playScrollBgFlowDown = false; + + function playScrollBgMapEligible() { + return !!(mapData && mapData.gameType === 'space_shooter' && (currentPlayMapId() || '').trim() === PLAY_SCROLL_BG_MAP_ID); + } + + function playScrollBgDrawActive() { + if (!playScrollBgMapEligible() || playScrollBgOff) return false; + const a = playScrollBgIntroImg; + const b = playScrollBgLoopImg; + return !!(a && a.complete && a.naturalWidth && b && b.complete && b.naturalWidth); + } + + function reloadPlayScrollBgFromMap() { + playScrollBgIntroImg = null; + playScrollBgLoopImg = null; + playScrollBgPx = 0; + if (!playScrollBgMapEligible()) return; + const raw = mapData.editorBgScroll && typeof mapData.editorBgScroll === 'object' ? mapData.editorBgScroll : {}; + playScrollBgOff = raw.enabled === false; + playScrollBgSpeedPxPerSec = Math.max(8, Math.min(400, Math.floor(Number(raw.speedPxPerSec)) || 56)); + const dirRaw = String(raw.scrollDirection || raw.direction || 'up').toLowerCase(); + playScrollBgFlowDown = (dirRaw === 'down' || dirRaw === 'top' || dirRaw === 'toptobottom'); + if (playScrollBgOff) return; + const introSrc = (typeof raw.introImage === 'string' && raw.introImage.length) ? raw.introImage : PLAY_SCROLL_BG_DEFAULT_INTRO; + const loopSrc = (typeof raw.loopImage === 'string' && raw.loopImage.length) ? raw.loopImage : PLAY_SCROLL_BG_DEFAULT_LOOP; + let pending = 2; + const bump = () => { + pending--; + if (pending <= 0) { + playScrollBgSyncInitialScrollToBottom(); + try { draw(); } catch (e) { /* ignore */ } + } + }; + const im1 = new Image(); + im1.onload = () => { playScrollBgIntroImg = im1; bump(); }; + im1.onerror = () => { bump(); }; + im1.src = introSrc; + const im2 = new Image(); + im2.onload = () => { playScrollBgLoopImg = im2; bump(); }; + im2.onerror = () => { bump(); }; + im2.src = loopSrc; + } + + function playScrollBgSyncInitialScrollToBottom() { + const intro = playScrollBgIntroImg; + if (!canvas || !intro || !intro.complete || !intro.naturalWidth) return; + const cwR = Math.max(1, Math.round(canvas.width)); + const chR = Math.max(1, Math.round(canvas.height)); + const drawHIntro = Math.round(intro.naturalHeight * (cwR / intro.naturalWidth)); + /* ล่าง→บน: S = HI−ch ให้ขอบล่าง intro ชิดล่างจอ · บน→ล่าง (พลิก dest): S=0 ถึงได้ขอบล่าง intro ชิดล่างจอ */ + if (playScrollBgFlowDown) { + playScrollBgPx = 0; + } else { + playScrollBgPx = Math.max(0, drawHIntro - chR); + } + } + + function drawPlayScrollBgFullCanvas(cw, ch) { + const intro = playScrollBgIntroImg; + const loop = playScrollBgLoopImg; + if (!intro || !intro.complete || !loop || !loop.complete) return; + const cwR = Math.max(1, Math.round(cw)); + const chR = Math.max(1, Math.round(ch)); + const scaleI = cwR / intro.naturalWidth; + const drawHIntro = Math.round(intro.naturalHeight * scaleI); + const scaleL = cwR / loop.naturalWidth; + const drawHLoop = Math.max(1, Math.round(loop.naturalHeight * scaleL)); + const S = playScrollBgPx; + const vp0 = S; + const vp1 = S + chR; + const yLimit = vp1 + drawHLoop * 2; + const flowDown = playScrollBgFlowDown; + + function stripTopToDestY(stripTop, drawH) { + if (!flowDown) return stripTop - S; + return S + chR - (stripTop + drawH); + } + + ctx.save(); + ctx.imageSmoothingEnabled = false; + ctx.beginPath(); + ctx.rect(0, 0, cwR, chR); + ctx.clip(); + + if (vp0 < 0) { + let k = 1; + if (vp0 < -drawHLoop * 4) { + k = Math.max(1, Math.floor(-vp0 / drawHLoop) - 2); + } + for (; k < 50000; k++) { + const y0 = -k * drawHLoop; + if (y0 >= vp1 + drawHLoop) break; + if (y0 + drawHLoop <= vp0) continue; + const dy = Math.round(stripTopToDestY(y0, drawHLoop)); + ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, dy, cwR, drawHLoop); + } + } + + if (drawHIntro > vp0 && vp1 > 0) { + const dy = Math.round(stripTopToDestY(0, drawHIntro)); + ctx.drawImage(intro, 0, 0, intro.naturalWidth, intro.naturalHeight, 0, dy, cwR, drawHIntro); + } + + let y = drawHIntro; + if (y < yLimit && vp1 > drawHIntro) { + if (vp0 > drawHIntro) { + const n = Math.floor((vp0 - drawHIntro) / drawHLoop); + y = drawHIntro + Math.max(0, n - 1) * drawHLoop; + } + while (y < yLimit) { + const dy = Math.round(stripTopToDestY(y, drawHLoop)); + ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, dy, cwR, drawHLoop); + y += drawHLoop; + } + } + ctx.restore(); + } + + function tickPlayScrollBg(dtSec) { + if (!playScrollBgDrawActive()) return; + playScrollBgPx += (playScrollBgSpeedPxPerSec || 56) * dtSec; + } + + /** Last Light (mno9kb07) — พื้นหลังแนวนอน: start → loop 2→3→4 → finish ครั้งเดียว (ไม่วน) แล้วจบเกม + ซ่อนอุปสรรค (gauntletCrownRunwayBg ในแมป) */ + const GAUNTLET_CROWN_RUNWAY_BG_MAP_ID = 'mno9kb07'; + /** หลังหยุดเลื่อนที่จุด FINISH — รอก่อน latch + โฟลว์สรุป (มิลลิวิ) */ + const GAUNTLET_CROWN_RUNWAY_POST_STOP_MS = 2000; + const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_START = BASE + '/img/editor-bg-mno9kb07/start.png'; + const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP2 = BASE + '/img/editor-bg-mno9kb07/loop2.png'; + const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP3 = BASE + '/img/editor-bg-mno9kb07/loop3.png'; + const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP4 = BASE + '/img/editor-bg-mno9kb07/loop4.png'; + const GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_FINISH = BASE + '/img/editor-bg-mno9kb07/finish.png'; + let gauntletCrownRunwayBgImgs = [null, null, null, null, null]; + let gauntletCrownRunwayBgOff = false; + let gauntletCrownRunwayBgSpeedPxPerSec = 48; + let gauntletCrownRunwayBgScrollPx = 0; + let gauntletCrownRunwayBgFinishLatched = false; + /** >0 = หยุดเลื่อนแล้ว (timestamp ms) รอ POST_STOP ก่อน latch */ + let gauntletCrownRunwayBgStripFreezeSinceMs = 0; + /** พรีวิว embed: แสดง GCM ครั้งเดียวเมื่อรันเวย์ครบแนว (ไม่รอเซิร์ฟเวอร์) */ + let gauntletCrownRunwayClientMissionShown = false; + /** พรมแดง mno9kb07 — จำกัดขอบขวาของตัว (ขอบขวา footprint ไม่เกินสัดส่วนนี้ของความกว้างแมปเป็นช่อง) */ + const GAUNTLET_CROWN_HEIST_MAX_X_WORLD_FRAC = 0.8; + const GAUNTLET_CROWN_SCORE_POP_URL = BASE + '/img/gauntlet-assets/score-.png'; + let gauntletCrownScorePenaltyImg = null; + function ensureGauntletCrownScorePenaltyImgPlay() { + if (gauntletCrownScorePenaltyImg) return gauntletCrownScorePenaltyImg; + const im = new Image(); + im.src = GAUNTLET_CROWN_SCORE_POP_URL; + gauntletCrownScorePenaltyImg = im; + return im; + } + let lastGauntletCrownRunwayBgTickMs = 0; + /** จัด offset เลื่อนรันเวย์ให้จุดเกิดตรงกับงานศิลป์ START (รูป 1) — รีเซ็ตตอน howto / โหลดแมปใหม่ */ + let gauntletCrownRunwaySpawnScrollSnapped = false; + + function gauntletCrownRunwayBgMapEligiblePlay() { + return !!(mapData && mapData.gameType === 'gauntlet' && (currentPlayMapId() || '').trim() === GAUNTLET_CROWN_RUNWAY_BG_MAP_ID); + } + + function gauntletCrownRunwayBgNatDims(im) { + if (!im) return null; + if (im.tagName === 'CANVAS' && im.width > 0 && im.height > 0) return { nw: im.width, nh: im.height }; + if (im.complete && im.naturalWidth > 0 && im.naturalHeight > 0) return { nw: im.naturalWidth, nh: im.naturalHeight }; + return null; + } + + function gauntletCrownRunwayBgImageSlotReadyPlay(im) { + return !!gauntletCrownRunwayBgNatDims(im); + } + + function gauntletCrownRunwayBgStripWidthsAtCh(chR) { + const ch0 = Math.max(1, Math.round(chR)); + const wOf = (im) => { + const d = gauntletCrownRunwayBgNatDims(im); + if (!d) return 0; + return Math.max(1, Math.round(d.nw * (ch0 / d.nh))); + }; + return { + w0: wOf(gauntletCrownRunwayBgImgs[0]), + w1: wOf(gauntletCrownRunwayBgImgs[1]), + w2: wOf(gauntletCrownRunwayBgImgs[2]), + w3: wOf(gauntletCrownRunwayBgImgs[3]), + w4: wOf(gauntletCrownRunwayBgImgs[4]), + }; + } + + function gauntletCrownRunwayBgImagesReadyPlay() { + for (let i = 0; i < 5; i++) { + if (!gauntletCrownRunwayBgImageSlotReadyPlay(gauntletCrownRunwayBgImgs[i])) return false; + } + return true; + } + + /** ถ้า URL บนเซิร์ฟยังไม่มีไฟล์ (404) ให้แผ่นเลื่อนได้ — พื้นผิวนุ่ม ไม่ใส่ข้อความใหญ่ (กันเหมือน debug) */ + function ensureGauntletCrownRunwayBgPlaceholdersPlay() { + if (!gauntletCrownRunwayBgMapEligiblePlay() || gauntletCrownRunwayBgOff) return; + const W = 960; + const H = 540; + const hues = [352, 210, 165, 225, 38]; + for (let i = 0; i < 5; i++) { + if (gauntletCrownRunwayBgImageSlotReadyPlay(gauntletCrownRunwayBgImgs[i])) continue; + const c = document.createElement('canvas'); + c.width = W; + c.height = H; + const g = c.getContext('2d'); + const h0 = hues[i]; + const grd = g.createLinearGradient(0, 0, W, H * 0.9); + grd.addColorStop(0, `hsl(${h0},26%,20%)`); + grd.addColorStop(0.45, `hsl(${h0},18%,14%)`); + grd.addColorStop(1, `hsl(${h0},12%,10%)`); + g.fillStyle = grd; + g.fillRect(0, 0, W, H); + g.strokeStyle = 'rgba(255,255,255,0.06)'; + g.lineWidth = 1; + for (let x = 0; x < W; x += 48) { + g.beginPath(); + g.moveTo(x, 0); + g.lineTo(x + H * 0.04, H); + g.stroke(); + } + const vg = g.createRadialGradient(W * 0.45, H * 0.35, 20, W * 0.5, H * 0.55, W * 0.85); + vg.addColorStop(0, 'rgba(0,0,0,0)'); + vg.addColorStop(1, 'rgba(0,0,0,0.38)'); + g.fillStyle = vg; + g.fillRect(0, 0, W, H); + g.fillStyle = 'rgba(255,255,255,0.1)'; + g.font = '10px system-ui,sans-serif'; + g.textAlign = 'right'; + g.fillText(String(i + 1), W - 10, H - 8); + gauntletCrownRunwayBgImgs[i] = c; + } + } + + function gauntletCrownRunwayBgDrawActivePlay() { + if (!gauntletCrownRunwayBgMapEligiblePlay() || gauntletCrownRunwayBgOff) return false; + return gauntletCrownRunwayBgImagesReadyPlay(); + } + + function gauntletCrownRunwayBgHideObstaclesPlay() { + return gauntletCrownRunwayBgDrawActivePlay() && gauntletCrownRunwayBgFinishLatched; + } + + /** Last Light: หยุดแอนิเมชันวิ่งเมื่อหยุดที่ FINISH หรือ latch แล้ว */ + function gauntletCrownRunwayAvatarRunAllowedPlay() { + if (gauntletCrownRunwayBgMapEligiblePlay() && gauntletCrownRunwayBgDrawActivePlay()) { + if (gauntletCrownRunwayBgFinishLatched) return false; + if (gauntletCrownRunwayBgStripFreezeSinceMs > 0) return false; + return true; + } + return true; + } + + function drawImageCoverCanvasPlay(ctx, img, cwR, chR) { + const d = gauntletCrownRunwayBgNatDims(img); + if (!d) return; + const iw = d.nw; + const ih = d.nh; + if (!iw || !ih) return; + const scale = Math.max(cwR / iw, chR / ih); + const dw = iw * scale; + const dh = ih * scale; + const dx = (cwR - dw) / 2; + const dy = (chR - dh) / 2; + ctx.drawImage(img, 0, 0, iw, ih, dx, dy, dw, dh); + } + + function drawRunwayHSlicePlay(ctx, img, worldX0, segW, scrollLeft, cwR, chR) { + const d0 = gauntletCrownRunwayBgNatDims(img); + if (!d0 || segW <= 0) return; + const natW = d0.nw; + const natH = d0.nh; + const v0 = scrollLeft; + const v1 = scrollLeft + cwR; + const o0 = Math.max(worldX0, v0); + const o1 = Math.min(worldX0 + segW, v1); + if (o1 <= o0) return; + const u0 = ((o0 - worldX0) / segW) * natW; + const u1 = ((o1 - worldX0) / segW) * natW; + const dx = o0 - scrollLeft; + const dw = o1 - o0; + /** ทาบแนวตั้งเล็กน้อยกันช่องว่างย่อย 1px (seam) ระหว่างแถบลูป */ + ctx.drawImage(img, u0, 0, u1 - u0, natH, dx - 0.25, 0, dw + 0.55, chR); + } + + /** วาดแถบรันเวย์ในระบบพิกัด X = ความกว้างมองเห็น (cwR) × สูงแถบ chR · scrollView = จุดเริ่ม viewport ในโลกรันเวย์ */ + function drawGauntletCrownRunwayStripCorePlay(ctx, cwR, chR, scrollView) { + const start = gauntletCrownRunwayBgImgs[0]; + const L2 = gauntletCrownRunwayBgImgs[1]; + const L3 = gauntletCrownRunwayBgImgs[2]; + const L4 = gauntletCrownRunwayBgImgs[3]; + const { w0, w1, w2, w3 } = gauntletCrownRunwayBgStripWidthsAtCh(chR); + const cycle = w1 + w2 + w3; + const S = scrollView; + ctx.imageSmoothingEnabled = true; + ctx.fillStyle = '#1a1b26'; + ctx.fillRect(0, 0, cwR, chR); + if (w0 > 0) drawRunwayHSlicePlay(ctx, start, 0, w0, S, cwR, chR); + if (cycle <= 0) return; + const ws = [w1, w2, w3]; + const ims = [L2, L3, L4]; + const uTotal = Math.max(0, S - w0); + let cycN = Math.floor(uTotal / cycle); + let uRem = uTotal - cycN * cycle; + let si = 0; + while (si < 3 && uRem >= ws[si]) { + uRem -= ws[si]; + si++; + } + if (si >= 3) { + cycN++; + si = 0; + uRem = 0; + } + let acc = 0; + for (let k = 0; k < si; k++) acc += ws[k]; + let curLeft = w0 + cycN * cycle + acc; + let guard = 0; + let idx = si; + while (curLeft < S + cwR && guard++ < 500) { + drawRunwayHSlicePlay(ctx, ims[idx], curLeft, ws[idx], S, cwR, chR); + curLeft += ws[idx]; + idx = (idx + 1) % 3; + } + } + + /** Last Light mno9kb07: แถบเดียว START → 2 → 3 → 4 → FINISH (ไม่วนลูป) */ + function drawGauntletCrownRunwayStripLinearOncePlay(ctx, cwR, chR, scrollView) { + const S = scrollView; + const { w0, w1, w2, w3, w4 } = gauntletCrownRunwayBgStripWidthsAtCh(chR); + ctx.imageSmoothingEnabled = true; + ctx.fillStyle = '#1a1b26'; + ctx.fillRect(0, 0, cwR, chR); + const segs = [ + { im: gauntletCrownRunwayBgImgs[0], w: w0, x0: 0 }, + { im: gauntletCrownRunwayBgImgs[1], w: w1, x0: w0 }, + { im: gauntletCrownRunwayBgImgs[2], w: w2, x0: w0 + w1 }, + { im: gauntletCrownRunwayBgImgs[3], w: w3, x0: w0 + w1 + w2 }, + { im: gauntletCrownRunwayBgImgs[4], w: w4, x0: w0 + w1 + w2 + w3 }, + ]; + for (let i = 0; i < segs.length; i++) { + const g = segs[i]; + if (g.w > 0 && g.im) drawRunwayHSlicePlay(ctx, g.im, g.x0, g.w, S, cwR, chR); + } + } + + /** + * @param {number} canvasW + * @param {number} canvasH + * @param {null|{ worldMinX:number, worldMaxX:number, camX:number, camY:number, zDraw:number, mapWpx:number, mapHpx:number }} worldAlign — ถ้ามี จะคลิป+สเกลให้ตรงกริดแมป (ตัว/สิ่งกีดขวางตรงพื้นที่ตั้ง) + */ + function drawGauntletCrownRunwayBgFullCanvasPlay(canvasW, canvasH, worldAlign) { + const finish = gauntletCrownRunwayBgImgs[4]; + const align = worldAlign && typeof worldAlign === 'object' + && Number.isFinite(worldAlign.worldMinX) + && Number.isFinite(worldAlign.worldMaxX) + && Number.isFinite(worldAlign.camX) + && Number.isFinite(worldAlign.camY) + && Number.isFinite(worldAlign.zDraw) + && Number.isFinite(worldAlign.mapWpx) + && Number.isFinite(worldAlign.mapHpx); + if (gauntletCrownRunwayBgFinishLatched && gauntletCrownRunwayBgImageSlotReadyPlay(finish)) { + if (align) { + const { camX, camY, zDraw, mapWpx, mapHpx } = worldAlign; + ctx.fillStyle = '#1a1b26'; + ctx.fillRect(0, 0, canvasW, canvasH); + ctx.save(); + /* เมทริกซ์เดียวกับ worldToScreen + คลิปโลก [0,map] — รูปจบครอบทั้งแผนที่ที่ world origin */ + ctx.setTransform(zDraw, 0, 0, zDraw, canvasW / 2 - camX * zDraw, canvasH / 2 - camY * zDraw); + ctx.beginPath(); + ctx.rect(0, 0, mapWpx, mapHpx); + ctx.clip(); + drawImageCoverCanvasPlay(ctx, finish, Math.max(1, Math.round(mapWpx)), Math.max(1, Math.round(mapHpx))); + ctx.restore(); + } else { + drawImageCoverCanvasPlay(ctx, finish, Math.max(1, Math.round(canvasW)), Math.max(1, Math.round(canvasH))); + } + return; + } + /* ใช้ความกว้างมองเห็นแบบ float ให้เท่ากับ canvas.width/zDraw — ห้าม Math.round ไม่งั้นซูมแล้วแถบ runway กว้างไม่ตรง worldToScreen (แผนหลุดจากตัว) */ + const cwR = align + ? Math.max(1e-6, worldAlign.worldMaxX - worldAlign.worldMinX) + : Math.max(1, Math.round(canvasW)); + const chR = align + ? Math.max(1, Math.round(worldAlign.mapHpx)) + : Math.max(1, Math.round(canvasH)); + const scrollView = align + ? (gauntletCrownRunwayBgScrollPx + worldAlign.worldMinX) + : gauntletCrownRunwayBgScrollPx; + if (align) { + const { camX, camY, zDraw, mapWpx, mapHpx, worldMinX } = worldAlign; + ctx.fillStyle = '#1a1b26'; + ctx.fillRect(0, 0, canvasW, canvasH); + ctx.save(); + ctx.setTransform(zDraw, 0, 0, zDraw, canvasW / 2 - camX * zDraw, canvasH / 2 - camY * zDraw); + ctx.beginPath(); + ctx.rect(0, 0, mapWpx, mapHpx); + ctx.clip(); + ctx.translate(worldMinX, 0); + if (gauntletCrownRunwayBgMapEligiblePlay()) { + drawGauntletCrownRunwayStripLinearOncePlay(ctx, cwR, chR, scrollView); + } else { + drawGauntletCrownRunwayStripCorePlay(ctx, cwR, chR, scrollView); + } + ctx.restore(); + return; + } + ctx.save(); + if (gauntletCrownRunwayBgMapEligiblePlay()) { + drawGauntletCrownRunwayStripLinearOncePlay(ctx, cwR, chR, scrollView); + } else { + drawGauntletCrownRunwayStripCorePlay(ctx, cwR, chR, scrollView); + } + ctx.restore(); + } + + /** + * Shared runway strip math (viewport in strip space). null if not drawable. + * Map tuning: gauntletCrownRunwayBg.finishStopScreenFrac 0.08–0.96 = จุดกลางจอต้องถึงภายในแถบ FINISH ก่อนหยุด (ค่าเริ่ม ~0.56 ใกล้กล่องหยุดขวาเส้น FINISH บนงานศิลป์) + */ + function gauntletCrownRunwayScrollGeometryPlay() { + if (!mapData || !canvas || !gauntletCrownRunwayBgDrawActivePlay()) return null; + const zGaRaw = computePlayCameraZDrawPlay(); + const gCam = getGauntletCrownHeistGroupCameraCenterPxPlay(tileSize, canvas.width, canvas.height, zGaRaw); + if (!gCam) return null; + const halfW = canvas.width / (2 * zGaRaw); + const worldMinX = gCam.px - halfW; + const cwWorld = canvas.width / zGaRaw; + const h = mapData.height || 15; + const ts = mapData.tileSize || 32; + const mapHpx = Math.max(1, Math.round(h * ts)); + const { w0, w1, w2, w3, w4 } = gauntletCrownRunwayBgStripWidthsAtCh(mapHpx); + const w4Safe = Math.max(w4, 1); + const totalStrip = w0 + w1 + w2 + w3 + w4Safe; + if (!(totalStrip > 8)) return null; + const scrollView = gauntletCrownRunwayBgScrollPx + worldMinX; + const right = scrollView + cwWorld; + const finishStart = w0 + w1 + w2 + w3; + const geoMinScroll = finishStart + Math.max(0, cwWorld - w4Safe); + const wideVpPad = cwWorld > w4Safe * 1.02 ? w4Safe * 0.38 : 0; + const minScrollForFinishPass = geoMinScroll + wideVpPad; + return { + scrollView, right, cwWorld, w4Safe, totalStrip, finishStart, minScrollForFinishPass, + }; + } + + /** True when viewport center crosses “หยุดที่ FINISH” (ไม่ใช้แค่ขอบขวาถึงปลายแทร็ก) */ + function gauntletCrownRunwayScrollStripStopZoneReachedPlay() { + const g = gauntletCrownRunwayScrollGeometryPlay(); + if (!g) return false; + const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {}; + let frac = Number(raw.finishStopScreenFrac); + if (!Number.isFinite(frac)) frac = 0.56; + frac = Math.max(0.08, Math.min(0.96, frac)); + const targetCenter = g.finishStart + g.w4Safe * frac; + const center = g.scrollView + g.cwWorld * 0.5; + if (g.scrollView < g.minScrollForFinishPass - 0.01) return false; + if (center < targetCenter - 1.5) return false; + return true; + } + + function resetGauntletCrownRunwaySpawnScrollSnap() { + gauntletCrownRunwaySpawnScrollSnapped = false; + } + + /** + * จัด gauntletCrownRunwayBgScrollPx ครั้งแรกให้คอลัมน์เกิด (ซ้ายสุด) ตรง ~runwaySpawnAlignFrac ของความกว้างแถบ START (w0) + * ในแมป: gauntletCrownRunwayBg.runwaySpawnAlignFrac 0.02–0.95 (ค่าเริ่ม 0.32) + * Anchor: คอลัมน์ซ้ายสุดระหว่างช่องเกิดในแมปกับตำแหน่งจริงของทุกคนในรอบ (รวม me + บอทพรีวิว) — ถ้า me ถูก resolve ไปคอลัมน์ซ้ายกว่า min ของช่องวาด แต่ runway ยึดแค่ช่องวาด จะเห็นตัวลอยซ้ายของพรมแดง + * Use min(editor spawn column, floor of every alive entity x) so runway START art stays tied to where characters actually stand. + */ + function trySnapGauntletCrownRunwayScrollToSpawnsPlay() { + if (gauntletCrownRunwaySpawnScrollSnapped) return; + if (!gauntletCrownRunwayBgMapEligiblePlay() || gauntletCrownRunwayBgOff || !mapData) return; + if (!gauntletCrownRunwayBgImagesReadyPlay()) return; + const h = mapData.height || 15; + const ts = mapData.tileSize || 32; + const mapHpx = Math.max(1, Math.round(h * ts)); + const { w0 } = gauntletCrownRunwayBgStripWidthsAtCh(mapHpx); + if (!(w0 > 0)) return; + const slots = collectGauntletSpawnSlotsPlay(mapData); + let minTX = Infinity; + if (slots.length) minTX = Math.min(...slots.map((s) => s.x)); + if (Number.isFinite(me.x)) minTX = Math.min(minTX, Math.floor(me.x)); + others.forEach((o, id) => { + if (!o || o.gauntletEliminated) return; + if (Number.isFinite(o.x)) minTX = Math.min(minTX, Math.floor(o.x)); + }); + if (!Number.isFinite(minTX) || minTX === Infinity) minTX = 1; + const anchorWorldX = (Math.max(0, minTX) + 0.5) * ts; + const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {}; + const frac = Math.max(0.02, Math.min(0.95, Number(raw.runwaySpawnAlignFrac) || 0.32)); + gauntletCrownRunwayBgScrollPx = frac * w0 - anchorWorldX; + gauntletCrownRunwaySpawnScrollSnapped = true; + } + + function reloadGauntletCrownRunwayBgFromMap() { + gauntletCrownRunwayBgImgs = [null, null, null, null, null]; + gauntletCrownRunwayBgScrollPx = 0; + gauntletCrownRunwayBgFinishLatched = false; + gauntletCrownRunwayBgStripFreezeSinceMs = 0; + gauntletCrownRunwayClientMissionShown = false; + lastGauntletCrownRunwayBgTickMs = 0; + resetGauntletCrownRunwaySpawnScrollSnap(); + if (!gauntletCrownRunwayBgMapEligiblePlay()) return; + const raw = mapData.gauntletCrownRunwayBg && typeof mapData.gauntletCrownRunwayBg === 'object' ? mapData.gauntletCrownRunwayBg : {}; + gauntletCrownRunwayBgOff = raw.enabled === false; + gauntletCrownRunwayBgSpeedPxPerSec = Math.max(8, Math.min(400, Math.floor(Number(raw.speedPxPerSec)) || 48)); + if (gauntletCrownRunwayBgOff) return; + const srcs = [ + (typeof raw.startImage === 'string' && raw.startImage.length) ? raw.startImage : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_START, + (typeof raw.loopImage2 === 'string' && raw.loopImage2.length) ? raw.loopImage2 : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP2, + (typeof raw.loopImage3 === 'string' && raw.loopImage3.length) ? raw.loopImage3 : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP3, + (typeof raw.loopImage4 === 'string' && raw.loopImage4.length) ? raw.loopImage4 : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_LOOP4, + (typeof raw.finishImage === 'string' && raw.finishImage.length) ? raw.finishImage : GAUNTLET_CROWN_RUNWAY_BG_DEFAULT_FINISH, + ]; + let pending = 5; + const bump = () => { + pending--; + if (pending <= 0) { + ensureGauntletCrownRunwayBgPlaceholdersPlay(); + try { draw(); } catch (e) { /* ignore */ } + } + }; + for (let i = 0; i < 5; i++) { + const im = new Image(); + const idx = i; + im.onload = () => { + gauntletCrownRunwayBgImgs[idx] = im; + bump(); + }; + im.onerror = () => { bump(); }; + im.src = srcs[i]; + } + } + + function tickGauntletCrownRunwayBgPlay(dtSec) { + if (!gauntletCrownRunwayBgDrawActivePlay()) return; + /** พรีรัน (howto / countdown) — ห้ามเลื่อนก่อน GO · Mega Virus ใช้ shell เดียวกัน */ + if (usesCrownLobbyShellPlay() && gauntletCrownPregamePhase !== 'live') return; + if (gauntletCrownRunwayBgFinishLatched) return; + + if (gauntletCrownRunwayBgStripFreezeSinceMs > 0) { + if (Date.now() - gauntletCrownRunwayBgStripFreezeSinceMs >= GAUNTLET_CROWN_RUNWAY_POST_STOP_MS) { + gauntletCrownRunwayBgFinishLatched = true; + gauntletCrownRunwayBgStripFreezeSinceMs = 0; + if (playBotsEnabled() && gauntletCrownRunwayBgMapEligiblePlay() && !gauntletCrownRunwayClientMissionShown) { + gauntletCrownRunwayClientMissionShown = true; + window.setTimeout(function () { + try { + gauntletCrownPregamePhase = null; + gauntletCrownHowtoVisible = false; + const hto = document.getElementById('gauntlet-crown-howto-overlay'); + if (hto) hto.classList.add('is-hidden'); + const gcc = document.getElementById('gauntlet-crown-countdown'); + if (gcc) gcc.classList.add('is-hidden'); + if (gauntletCrownCountdownTimer) { + clearTimeout(gauntletCrownCountdownTimer); + gauntletCrownCountdownTimer = null; + } + showGauntletCrownMissionOverlay(gauntletCrownHeistBuildLocalCrownMissionPlay()); + } catch (_e) { /* ignore */ } + try { + draw(); + } catch (_e2) { /* ignore */ } + }, 340); + } + } + return; + } + + if (gauntletCrownRunwayBgMapEligiblePlay() && gauntletCrownRunwayScrollStripStopZoneReachedPlay()) { + gauntletCrownRunwayBgStripFreezeSinceMs = Date.now(); + try { + syncGauntletCrownJumpButton(); + } catch (_sj) { /* ignore */ } + try { + draw(); + } catch (_d) { /* ignore */ } + return; + } + gauntletCrownRunwayBgScrollPx += (gauntletCrownRunwayBgSpeedPxPerSec || 48) * dtSec; + } + + /** stack + แมป mnn93hpi — พื้นหลัง intro+loop เลื่อนแนวตั้ง (stackTowerBgScroll ในแมป) */ + const STACK_TOWER_SCROLL_BG_DEFAULT_INTRO = BASE + '/img/editor-bg-mnn93hpi/intro.png'; + const STACK_TOWER_SCROLL_BG_DEFAULT_LOOP = BASE + '/img/editor-bg-mnn93hpi/loop.png'; + let stackTowerScrollBgIntroImg = null; + let stackTowerScrollBgLoopImg = null; + let stackTowerScrollBgPx = 0; + let stackTowerScrollBgSpeedPxPerSec = 40; + let stackTowerScrollBgOff = false; + let stackTowerScrollBgFlowDown = false; + /** Sraw = px+boost+post ครั้งแรกที่คำนวณ world offset — ใช้หา delta แบบคาบ loop (อย่าใช้ fold(Sraw) ลบตรงๆ จะดันโลกขึ้นหลายร้อย px) */ + let stackTowerScrollBgSrawWorldBaselinePlay = null; + /** ความสูงวาดของไทล์ loop (พิกเซลจอ) — ใช้ขั้นเลื่อนเมื่อ stepScrollPx = 0 */ + let stackTowerScrollBgLoopDrawHPlay = 0; + /** { from, to, t0, dur } — เลื่อนแถบ BG เป็นขั้นตามจำนวนชั้น */ + let stackTowerBgScrollStepAnimPlay = null; + let stackTowerBgScrollPendingStepDeltaPlay = 0; + + function stackTowerScrollBgMapEligible() { + return !!(mapData && mapData.gameType === 'stack' && (currentPlayMapId() || '').trim() === STACK_TOWER_MISSION_MAP_ID); + } + + function stackTowerScrollBgImagesReady() { + if (!stackTowerScrollBgMapEligible()) return false; + const a = stackTowerScrollBgIntroImg; + const b = stackTowerScrollBgLoopImg; + return !!(a && a.complete && a.naturalWidth && b && b.complete && b.naturalWidth); + } + + /** แถบ intro+loop เต็มจอ — เปิดเฉพาะเมื่อแมปตั้ง stackTowerBgScroll.enabled: true และรูปโหลดครบ (ไม่โหลด strip เมื่อปิด → ใช้รูปแผนที่ backgroundImage) */ + function stackTowerScrollBgDrawActive() { + return stackTowerScrollBgImagesReady() && !stackTowerScrollBgOff; + } + + function reloadStackTowerScrollBgFromMap() { + stackTowerScrollBgIntroImg = null; + stackTowerScrollBgLoopImg = null; + stackTowerScrollBgPx = 0; + stackTowerScrollBgSrawWorldBaselinePlay = null; + stackTowerScrollBgLoopDrawHPlay = 0; + stackTowerBgScrollStepAnimPlay = null; + stackTowerBgScrollPendingStepDeltaPlay = 0; + if (!stackTowerScrollBgMapEligible()) return; + const raw = mapData.stackTowerBgScroll && typeof mapData.stackTowerBgScroll === 'object' ? mapData.stackTowerBgScroll : {}; + stackTowerScrollBgOff = raw.enabled !== true; + { + const spNum = Number(raw.speedPxPerSec); + stackTowerScrollBgSpeedPxPerSec = Number.isFinite(spNum) + ? Math.max(0, Math.min(400, Math.floor(spNum))) + : 0; + } + /* คีย์เวิร์ด down/top = ตัวเลือก "บน→ล่าง" ในเอดิเตอร์ — ใช้สูตรเลื่อนแบบ stripTop−S (ฉากไหลขึ้น) ไม่ใช่ S+chR−… (ไหลลง) เพราะผู้เล่นคาดว่าหอสูง = มองขึ้น */ + const dirRaw = String(raw.scrollDirection || raw.direction || 'down').toLowerCase(); + const rawIsDownKeyword = (dirRaw === 'down' || dirRaw === 'top' || dirRaw === 'toptobottom'); + stackTowerScrollBgFlowDown = !rawIsDownKeyword; + if (stackTowerScrollBgOff) return; + const introSrc = (typeof raw.introImage === 'string' && raw.introImage.length) ? raw.introImage : STACK_TOWER_SCROLL_BG_DEFAULT_INTRO; + const loopSrc = (typeof raw.loopImage === 'string' && raw.loopImage.length) ? raw.loopImage : STACK_TOWER_SCROLL_BG_DEFAULT_LOOP; + let pending = 2; + const bump = () => { + pending--; + if (pending <= 0) { + const intro0 = stackTowerScrollBgIntroImg; + const loop0 = stackTowerScrollBgLoopImg; + if (loop0 && loop0.complete && intro0 && intro0.complete && canvas && canvas.width) { + const cwR0 = Math.max(1, Math.round(canvas.width)); + stackTowerScrollBgLoopDrawHPlay = Math.max(1, Math.round(loop0.naturalHeight * (cwR0 / loop0.naturalWidth))); + } + stackTowerScrollBgSyncInitialScrollToBottom(); + try { draw(); } catch (e) { /* ignore */ } + } + }; + const im1 = new Image(); + im1.onload = () => { stackTowerScrollBgIntroImg = im1; bump(); }; + im1.onerror = () => { bump(); }; + im1.src = introSrc; + const im2 = new Image(); + im2.onload = () => { stackTowerScrollBgLoopImg = im2; bump(); }; + im2.onerror = () => { bump(); }; + im2.src = loopSrc; + } + + function stackTowerScrollBgSyncInitialScrollToBottom() { + const intro = stackTowerScrollBgIntroImg; + if (!canvas || !intro || !intro.complete || !intro.naturalWidth) return; + const cwR = Math.max(1, Math.round(canvas.width)); + const chR = Math.max(1, Math.round(canvas.height)); + const drawHIntro = Math.round(intro.naturalHeight * (cwR / intro.naturalWidth)); + if (stackTowerScrollBgFlowDown) { + stackTowerScrollBgPx = 0; + } else { + /* ขอบล่าง intro ชิดขอบล่างแคนวาส — คู่กับ stripTop−S */ + stackTowerScrollBgPx = drawHIntro - chR; + } + } + + /** + * พับค่าเลื่อนแนวตั้ง S (หน่วยเดียวกับ drawStackTowerScrollBgFullCanvas) เข้าช่วงหนึ่งคาบของ loop + * เมื่อ vp0 ≥ drawHIntro — รูปบนจอซ้ำทุก drawHLoop; พับค่า S ก่อนส่งเข้า worldToScreen กัน px โตไม่จำกัดดันโลกหลุดจอ + */ + function foldStackTowerScrollStripViewSForPhase(S, drawHIntro, drawHLoop) { + const dH = Math.max(1, Math.round(Number(drawHLoop) || 1)); + const dI = Math.max(0, Math.round(Number(drawHIntro) || 0)); + const s = Number(S) || 0; + if (s < dI) return s; + return s - Math.floor((s - dI) / dH) * dH; + } + + /** + * offset แนวตั้งจอให้โลกซิงก์กับ draw ที่ใช้ S = fold(Sraw) — ใช้ความต่างเฟส fold(Sraw)−fold(Sraw0) + * เวลาเลื่อนครบคาบ loop ค่า fold วนกลับ → offset ไม่โตไม่จำกัด · ห้ามคืน fold(Sraw) ล้วนเมื่อ Sraw0 ยังอยู่ intro (baseline ไม่เข้า loop ทำให้สูตร f0+mod ไม่ทำงานและเฟสหลุด) + */ + function stackTowerScrollWorldTotalYFromStripPlay(Sraw, drawHIntro, drawHLoop, Sraw0) { + if (Sraw0 == null) return 0; + const dH = Math.max(1, Math.round(Number(drawHLoop) || 1)); + const dI = Math.max(0, Math.round(Number(drawHIntro) || 0)); + const s = Number(Sraw) || 0; + const s0 = Number(Sraw0) || 0; + const fs = foldStackTowerScrollStripViewSForPhase(s, dI, dH); + const fs0 = foldStackTowerScrollStripViewSForPhase(s0, dI, dH); + return fs - fs0; + } + + /** zDrawPan ส่งต่อเพื่อคูณ boost กับ z — ไม่ scale แคนวาสทั้งแผง (เคย scale รอบกลางทำให้จอบางส่วนว่างเมื่อ z ≠ zRef) */ + function drawStackTowerScrollBgFullCanvas(cw, ch, zDrawPan) { + const intro = stackTowerScrollBgIntroImg; + const loop = stackTowerScrollBgLoopImg; + if (!intro || !intro.complete || !loop || !loop.complete) return; + const zPan = Number(zDrawPan) > 0 ? zDrawPan : zoom; + const cwR = Math.max(1, Math.round(cw)); + const chR = Math.max(1, Math.round(ch)); + const scaleI = cwR / intro.naturalWidth; + const drawHIntro = Math.round(intro.naturalHeight * scaleI); + const scaleL = cwR / loop.naturalWidth; + const drawHLoop = Math.max(1, Math.round(loop.naturalHeight * scaleL)); + const camFollowScreen = isStackTowerMissionUiMapPlay() ? getStackTowerBgScrollHeightBoostPx() * zPan : 0; + const post50Scroll = isStackTowerMissionUiMapPlay() ? getStackTowerPost50BgScrollExtraPxPlay(chR) : 0; + const Sraw = stackTowerScrollBgPx + camFollowScreen + post50Scroll; + const S = foldStackTowerScrollStripViewSForPhase(Sraw, drawHIntro, drawHLoop); + const vp0 = S; + const vp1 = S + chR; + const yLimit = vp1 + drawHLoop * 96; + const flowDown = stackTowerScrollBgFlowDown; + + function stripTopToDestY(stripTop, drawH) { + if (!flowDown) return stripTop - S; + return S + chR - (stripTop + drawH); + } + + ctx.save(); + ctx.imageSmoothingEnabled = false; + ctx.beginPath(); + ctx.rect(0, 0, cwR, chR); + ctx.clip(); + + if (vp0 < 0) { + let k = 1; + if (vp0 < -drawHLoop * 4) { + k = Math.max(1, Math.floor(-vp0 / drawHLoop) - 2); + } + for (; k < 50000; k++) { + const y0 = -k * drawHLoop; + if (y0 >= vp1 + drawHLoop) break; + if (y0 + drawHLoop <= vp0) continue; + const dy = Math.round(stripTopToDestY(y0, drawHLoop)); + ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, dy, cwR, drawHLoop); + } + } + + if (drawHIntro > vp0 && vp1 > 0) { + const dy = Math.round(stripTopToDestY(0, drawHIntro)); + ctx.drawImage(intro, 0, 0, intro.naturalWidth, intro.naturalHeight, 0, dy, cwR, drawHIntro); + } + + let y = drawHIntro; + if (y < yLimit && vp1 > drawHIntro) { + if (vp0 > drawHIntro) { + const n = Math.floor((vp0 - drawHIntro) / drawHLoop); + /* ใช้ n ไม่ใช่ n−1 — เดิมทำให้แถบ loop เริ่มสูงกว่า vp0 หนึ่งความสูง จอล่างว่างดำเมื่อ S ใหญ่ */ + y = drawHIntro + n * drawHLoop; + } + while (y < yLimit) { + const dy = Math.round(stripTopToDestY(y, drawHLoop)); + ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, dy, cwR, drawHLoop); + y += drawHLoop; + } + } + ctx.restore(); + } + + /** กล้องมองเหนือขอบบนแมป (worldMinY<0) + ใช้รูปแผนที่ — ยัด loop ท้องฟ้าเติมครึ่งบนจอ */ + function drawStackTowerLoopSkyFillAboveMap(ctx, worldMinY, zDraw, cw, ch) { + if (!isStackTowerMissionUiMapPlay() || worldMinY >= 0) return; + const loop = stackTowerScrollBgLoopImg; + if (!loop || !loop.complete || !loop.naturalWidth) return; + const cwR = Math.max(1, Math.round(cw)); + const chR = Math.max(1, Math.round(ch)); + const scaleL = cwR / loop.naturalWidth; + const drawHLoop = Math.max(1, Math.round(loop.naturalHeight * scaleL)); + const skyScreenH = Math.min(chR, Math.ceil(-worldMinY * zDraw) + 4); + const stripPx = stackTowerScrollBgOff ? 0 : stackTowerScrollBgPx; + const rawScroll = stripPx + getStackTowerBgScrollHeightBoostPx() * zDraw + + getStackTowerPost50BgScrollExtraPxPlay(chR); + const scroll = ((rawScroll % drawHLoop) + drawHLoop) % drawHLoop; + let y = -scroll; + ctx.save(); + ctx.imageSmoothingEnabled = false; + while (y < skyScreenH + drawHLoop) { + if (y + drawHLoop > 0) { + ctx.drawImage(loop, 0, 0, loop.naturalWidth, loop.naturalHeight, 0, y, cwR, drawHLoop); + } + y += drawHLoop; + } + ctx.restore(); + } + + function tickStackTowerBgScrollStepAnimPlay() { + const q = stackTowerBgScrollStepAnimPlay; + if (!q) return; + const t = Math.min(1, (performance.now() - q.t0) / Math.max(1, q.dur)); + const e = 1 - Math.pow(1 - t, 3); + stackTowerScrollBgPx = q.from + (q.to - q.from) * e; + if (t >= 1) { + stackTowerScrollBgPx = q.to; + stackTowerBgScrollStepAnimPlay = null; + stackTowerScrollBgSrawWorldBaselinePlay = null; + if (stackTowerBgScrollPendingStepDeltaPlay !== 0) { + const d = stackTowerBgScrollPendingStepDeltaPlay; + stackTowerBgScrollPendingStepDeltaPlay = 0; + startStackTowerBgScrollStepAnimPlay(d); + } + } + } + + function getStackTowerBgScrollStepEveryLayersPlay() { + const raw = mapData && mapData.stackTowerBgScroll; + if (!raw || typeof raw !== 'object') return 0; + const n = Math.floor(Number(raw.stepEveryLayers)); + return Number.isFinite(n) && n > 0 ? Math.min(200, n) : 0; + } + + function getStackTowerBgScrollStepAnimMsPlay() { + const raw = mapData && mapData.stackTowerBgScroll; + if (!raw || typeof raw !== 'object') return 520; + const t = Math.floor(Number(raw.stepAnimMs)); + return Number.isFinite(t) ? Math.max(120, Math.min(4000, t)) : 520; + } + + function getStackTowerBgScrollStepScrollPxPlay() { + const raw = mapData && mapData.stackTowerBgScroll; + if (!raw || typeof raw !== 'object') return 0; + const d = Math.floor(Number(raw.stepScrollPx)); + return Number.isFinite(d) ? Math.max(0, Math.min(8000, d)) : 0; + } + + function startStackTowerBgScrollStepAnimPlay(deltaPx) { + if (!stackTowerScrollBgDrawActive() || !deltaPx) return; + if (stackTowerBgScrollStepAnimPlay) { + stackTowerBgScrollPendingStepDeltaPlay += deltaPx; + return; + } + const dur = getStackTowerBgScrollStepAnimMsPlay(); + stackTowerBgScrollStepAnimPlay = { + from: stackTowerScrollBgPx, + to: stackTowerScrollBgPx + deltaPx, + t0: performance.now(), + dur, + }; + } + + function maybeQueueStackTowerBgScrollStepPlay(layerCountAfter) { + if (!stackTowerScrollBgDrawActive()) return; + const every = getStackTowerBgScrollStepEveryLayersPlay(); + if (every <= 0 || layerCountAfter <= 0 || layerCountAfter % every !== 0) return; + let delta = getStackTowerBgScrollStepScrollPxPlay(); + if (!delta) delta = Math.max(8, stackTowerScrollBgLoopDrawHPlay || 200); + delta = Math.min(8000, Math.max(8, delta)); + startStackTowerBgScrollStepAnimPlay(delta); + } + + function getStackTowerStripReleaseGapWorldPxPlay() { + const raw = mapData && mapData.stackTowerBgScroll; + if (!isStackTowerMissionUiMapPlay() || !raw || typeof raw !== 'object') return null; + const g = Number(raw.releaseGapWorldPx); + if (!Number.isFinite(g) || g < 0) return null; + return Math.min(800, g); + } + + function updateStackTowerSwingYForStripGapPlay() { + if (!stackMini || !isStackTowerMissionUiMapPlay()) return; + const gap = getStackTowerStripReleaseGapWorldPxPlay(); + if (gap == null) return; + const m = stackMini; + const lh = m.layerWorldH || Math.max(14, tileSize * 0.3); + const nLay = m.layers ? m.layers.length : 0; + const topBlockTopY = m.floorWorldY - nLay * lh; + const swingLift = getStackTowerSwingLiftWorldPx(nLay, lh); + m.swingWorldY = topBlockTopY - gap + swingLift; + } + + function tickStackTowerScrollBg(dtSec) { + if (!stackTowerScrollBgMapEligible() || stackTowerScrollBgOff) return; + if (!stackTowerScrollBgImagesReady()) return; + tickStackTowerBgScrollStepAnimPlay(); + if (stackTowerBgScrollStepAnimPlay) return; + const spd = Math.max(0, Number(stackTowerScrollBgSpeedPxPerSec) || 0) * dtSec; + if (spd <= 0) return; + /* !flowDown (stripTop−S): S เพิ่ม → dy ลด → ฉากไหลขึ้น — ห้ามใช้ −= (จะลด S แล้วศิลป์ไหลลง) */ + stackTowerScrollBgPx += spd; + } + + /** + * ค่าลบจาก screen Y ใน worldToScreen — ซิงก์กับ drawStackTowerScrollBgFullCanvas (auto + boost + post-50) + * แถบ BG วาด 1:1 กับแคนวาส — ห้ามใช้ fold(Sraw) เป็นค่าลบตรงๆ (จะดันโลกขึ้นหลายร้อย px เหมือนครึ่งล่างจอว่าง) + */ + function getStackTowerWorldLayerScrollScreenOffsetYPlay(zPan) { + if (!isStackTowerMissionUiMapPlay() || !stackMini) return 0; + const zPn = Number(zPan) > 0 ? zPan : zoom; + const chPx = Math.max(1, Math.round(canvas && canvas.height ? canvas.height : 720)); + const boostScreen = getStackTowerBgScrollHeightBoostPx() * zPn; + const post50Screen = getStackTowerPost50BgScrollExtraPxPlay(chPx); + const intro = stackTowerScrollBgIntroImg; + const loop = stackTowerScrollBgLoopImg; + let totalScreen = boostScreen + post50Screen; + if (stackTowerScrollBgDrawActive() && intro && intro.complete && intro.naturalWidth + && loop && loop.complete && loop.naturalWidth && canvas && canvas.width) { + const cwR = Math.max(1, Math.round(canvas.width)); + const drawHIntro = Math.round(intro.naturalHeight * (cwR / intro.naturalWidth)); + const drawHLoop = Math.max(1, Math.round(loop.naturalHeight * (cwR / loop.naturalWidth))); + const SrawDraw = stackTowerScrollBgPx + boostScreen + post50Screen; + if (stackTowerScrollBgSrawWorldBaselinePlay == null) stackTowerScrollBgSrawWorldBaselinePlay = SrawDraw; + const S0 = stackTowerScrollBgSrawWorldBaselinePlay; + let stripY; + if (stackTowerScrollBgFlowDown) { + const Sview = foldStackTowerScrollStripViewSForPhase(SrawDraw, drawHIntro, drawHLoop); + stripY = (boostScreen + post50Screen) * 2 - Sview; + } else { + stripY = stackTowerScrollWorldTotalYFromStripPlay(SrawDraw, drawHIntro, drawHLoop, S0); + } + totalScreen = stripY; + } else if (stackTowerScrollBgDrawActive()) { + const px = stackTowerScrollBgFlowDown ? -stackTowerScrollBgPx : stackTowerScrollBgPx; + totalScreen = px + boostScreen + post50Screen; + } + return totalScreen; + } + + /** โหลดจาก mapData.gridImageLibrary — ดัชนีตรงกับ gridImageCells[y][x] */ + let mapGridImageImgs = []; + let mapGridImageHeldImgs = []; + let me = { x: 1, y: 1, direction: 'down', nickname: nick, isWalking: false, playTint: null, gauntletScore: 0, gauntletEliminated: false, tx: null, ty: null, quizCarryHeld: null, gauntletCrownPenaltyFxUntil: 0 }; + const others = new Map(); + /** Stack preview HUD: เทอร์มินัลล็อก (cyber UI) */ + const STACK_PREVIEW_HUD_LOG_MAX = 8; + let stackPreviewHudLog = []; + function stackPreviewPushHudLog(line) { + if (!playBotsEnabled()) return; + stackPreviewHudLog.push(String(line || '').slice(0, 96)); + while (stackPreviewHudLog.length > STACK_PREVIEW_HUD_LOG_MAX) stackPreviewHudLog.shift(); + } + function stackPreviewLogStackDrop(hit, actor) { + if (!playBotsEnabled() || !hit) return; + const bid = actor && actor.botId; + let nm = 'NODE'; + if (actor && actor.human) nm = (me.nickname || 'YOU').toUpperCase().replace(/\s+/g, '_').slice(0, 12); + else if (bid && others.get(bid)) nm = String(others.get(bid).nickname || 'BOT').toUpperCase().replace(/\s+/g, '_').slice(0, 12); + if (hit.miss) stackPreviewPushHudLog(`>>> ${nm}: SECTOR_MISS`); + else if (hit.perfect) stackPreviewPushHudLog(`>>> ${nm}: PERFECT +${hit.pts} SYN`); + else stackPreviewPushHudLog(`>>> ${nm}: LOCK +${hit.pts}`); + } + const keys = {}; + const characterImages = {}; + /** data URL จาก canvas compose สี — กันยิง toDataURL ทุกเฟรมตอน sync HUD */ + const cyberHudScoreAvatarUrlCache = new Map(); + const CYBER_HUD_AV_URL_CACHE_CAP = 56; + function createDefaultAvatarImg() { + const c = document.createElement('canvas'); + c.width = 64; c.height = 64; + const ctx = c.getContext('2d'); + ctx.fillStyle = '#7aa2f7'; + ctx.beginPath(); + ctx.arc(32, 22, 14, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#9ece6a'; + ctx.beginPath(); + ctx.arc(32, 48, 18, 0, Math.PI * 2); + ctx.fill(); + ctx.fillStyle = '#1a1b26'; + ctx.beginPath(); + ctx.arc(28, 20, 3, 0, Math.PI * 2); + ctx.arc(36, 20, 3, 0, Math.PI * 2); + ctx.fill(); + const img = new Image(); + img.src = c.toDataURL('image/png'); + return img; + } + const defaultAvatarImg = createDefaultAvatarImg(); + const characterAnimations = {}; + const characterIdleAnimations = {}; + const CHARACTER_ANIM_FRAMES = 4; + const CHARACTER_ANIM_FRAME_MS = 200; + + /** จังหวะเดินลูปคงที่ 4 เฟรม — อย่า modulo ด้วยจำนวนเฟรมที่โหลดได้แล้ว (ไม่งั้นเฟรมกลางหาย/ลูปสั้นลง) */ + function walkAnimPhaseIndex(now, isWalking) { + const t = isWalking ? (typeof now === 'number' ? now : Date.now()) : 0; + return Math.floor(t / CHARACTER_ANIM_FRAME_MS) % CHARACTER_ANIM_FRAMES; + } + + /** เลือกเฟรมสูงสุดที่โหลดแล้วและ <= phase (เดิมถอย) */ + function pickLoadedWalkFrameIndex(anim, phase) { + if (!anim || !anim.frames || !anim.frames.length) return -1; + const maxK = Math.min(phase, anim.frames.length - 1, CHARACTER_ANIM_FRAMES - 1); + for (let k = maxK; k >= 0; k--) { + const f = anim.frames[k]; + if (f && f.complete && f.naturalWidth) return k; + } + return -1; + } + + /** ตัวละครอัปโหลดจากระบบมักเป็น char- + ตัวเลข (อาจมี suffix) — ไม่มี idle strip / idle layer ครบทุกทิศ */ + function isUploadedCharAssetId(id) { + if (!id) return false; + const s = String(id).trim(); + return /^char-\d/i.test(s); + } + + function useMultiFrameIdleSpriteSheets(id) { + if (!id) return true; + return !isUploadedCharAssetId(id); + } + + function ensureCharacterIdleAnim(id, dir) { + const d = dir || 'down'; + /* char-* ไม่มี *_idle_0.png — ใช้ strip เดิน dir_0..3 เป็น idle ต่อทิศ (มีลูป) แทนรูปเดียวที่มักไม่มี */ + if (!useMultiFrameIdleSpriteSheets(id)) { + const key = String(id) + '_' + d + '_idle'; + let anim = characterIdleAnimations[key]; + if (!anim) { + anim = { frames: [], fallback: null }; + characterIdleAnimations[key] = anim; + const enc = encodeURIComponent(id); + for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) { + const img = new Image(); + img.src = BASE + '/img/characters/' + enc + '_' + d + '_' + i + '.png'; + anim.frames.push(img); + } + const fb = new Image(); + fb.src = BASE + '/img/characters/' + enc + '_' + d + '_idle.png'; + anim.fallback = fb; + } + return anim; + } + const key = id + '_' + d + '_idle'; + let anim = characterIdleAnimations[key]; + if (!anim) { + anim = { frames: [], fallback: null }; + characterIdleAnimations[key] = anim; + const enc = encodeURIComponent(id); + for (let i = 0; i < CHARACTER_ANIM_FRAMES; i++) { + const img = new Image(); + img.src = BASE + '/img/characters/' + enc + '_' + d + '_idle_' + i + '.png'; + anim.frames.push(img); + } + const fb = new Image(); + fb.src = BASE + '/img/characters/' + enc + '_' + d + '_idle.png'; + anim.fallback = fb; + } + return anim; + } + + /** ลูป idle ขณะยืน — ใช้จังหวะเวลาเหมือนเดิน (CHARACTER_ANIM_FRAME_MS) */ + function idleAnimPhaseIndex(now) { + const t = typeof now === 'number' ? now : Date.now(); + return Math.floor(t / CHARACTER_ANIM_FRAME_MS) % CHARACTER_ANIM_FRAMES; + } + + function characterIdleSpritesVisible(id, dir) { + if (!id) return false; + const anim = ensureCharacterIdleAnim(id, dir || 'down'); + if (anim.fallback && anim.fallback.complete && anim.fallback.naturalWidth) return true; + for (let i = 0; i < anim.frames.length; i++) { + const f = anim.frames[i]; + if (f && f.complete && f.naturalWidth) return true; + } + return false; + } + + /** สุ่มสีตามเลเยอร์เดียวกับ character.html: bodyColor / hairColor / headColor (ลำดับซ้อนเหมือน composeLayeredFrame) */ + const PLAY_LAYER_ORDER = ['shadow', 'bodyColor', 'bodyStroke', 'headColor', 'headStroke', 'hairColor', 'hairStroke', 'face']; + const PLAY_LAYER_TINT_KEY = { bodyColor: 'body', headColor: 'head', hairColor: 'hair' }; + const PLAY_TINT_HEAD = ['#eaa78a', '#fbd5c4', '#fae9e1']; + const PLAY_TINT_HAIR = ['#d72520', '#ef8508', '#efe237', '#5bb443', '#2585cb', '#3f4ead', '#b53fd6', '#ef62b9']; + const PLAY_TINT_BODY = ['#fb4941', '#feaa11', '#fefe6d', '#adfd85', '#45fbfd', '#799afe', '#f87dff', '#fec1fe']; + const playLayerMode = {}; + /** คิว probe ครบ up/down/left/right แล้ว — กันทิศที่ไม่มีไฟล์ layer ไปตก fallback แถบทั้งทั้งที่ทิศอื่นมี */ + const playLayerAllDirsQueued = {}; + /** จาก GET /api/characters — hasLayerFiles สแกนจากชื่อไฟล์จริง (แม่นกว่า probe จาก อย่างเดียว) */ + let playCharLayerApi = { status: 'idle', map: null }; + /** id → Set เลเยอร์ที่มีไฟล์จริง (จาก GET /api/characters) — กันยิง URL เลเยอร์ที่ไม่เคยอัปโหลด → 404 รกคอนโซล */ + const playCharDiskLayersById = Object.create(null); + + function ensurePlayCharLayerListFetch() { + if (playCharLayerApi.status !== 'idle') return; + playCharLayerApi.status = 'loading'; + fetch(BASE + '/api/characters') + .then((r) => r.json()) + .then((list) => { + const map = Object.create(null); + playCharacterIdRoster = []; + if (Array.isArray(list)) { + list.forEach((c) => { + if (!c || !c.id) return; + playCharacterIdRoster.push(String(c.id)); + map[c.id] = !!c.hasLayerFiles; + if (Array.isArray(c.layers) && c.layers.length) { + const st = new Set(); + c.layers.forEach((n) => { + if (typeof n === 'string' && n.length) st.add(n); + }); + if (st.size) playCharDiskLayersById[c.id] = st; + else delete playCharDiskLayersById[c.id]; + } else { + delete playCharDiskLayersById[c.id]; + } + }); + } + playCharLayerApi = { status: 'done', map }; + }) + .catch(() => { + playCharacterIdRoster = []; + playCharLayerApi = { status: 'done', map: Object.create(null) }; + }); + } + + /** @returns {boolean|null} true/false จาก API, null = ยังไม่โหลดหรือไม่มีรายการ id นี้ */ + function playCharLayerFromApi(characterId) { + if (playCharLayerApi.status !== 'done' || !characterId) return null; + if (Object.prototype.hasOwnProperty.call(playCharLayerApi.map, characterId)) { + return playCharLayerApi.map[characterId]; + } + return null; + } + + /** ถ้าเซิร์ฟเวอร์ส่งรายชื่อเลเยอร์ที่มีบนดิสก์ — ไม่โหลดเลเยอร์อื่น (ลด 404) */ + function playCharDiskLayersSet(characterId) { + if (!characterId || playCharLayerApi.status !== 'done') return null; + const s = playCharDiskLayersById[characterId]; + return s && s.size ? s : null; + } + + const playLayerImageCache = new Map(); + const playLayerCompositeCache = new Map(); + function pickRandomPlayTint() { + return { + head: PLAY_TINT_HEAD[Math.floor(Math.random() * PLAY_TINT_HEAD.length)], + hair: PLAY_TINT_HAIR[Math.floor(Math.random() * PLAY_TINT_HAIR.length)], + body: PLAY_TINT_BODY[Math.floor(Math.random() * PLAY_TINT_BODY.length)], + }; + } + + /** FNV-1a 32-bit — แยกดัชนี head/hair/body ได้กระจายกว่า hash*31 เดิม (กันบอท __pv_bot_N สีซ้ำ) */ + function hashStringFnv1a32(str) { + let h = 2166136261 >>> 0; + const s = String(str || ''); + for (let i = 0; i < s.length; i++) { + h ^= s.charCodeAt(i); + h = Math.imul(h, 16777619) >>> 0; + } + return h >>> 0; + } + + function playTintFromPeerId(id) { + const s = String(id || 'x'); + const h0 = hashStringFnv1a32(s); + const h1 = hashStringFnv1a32(s + '\n1hair'); + const h2 = hashStringFnv1a32(s + '\n2body'); + return { + head: PLAY_TINT_HEAD[h0 % PLAY_TINT_HEAD.length], + hair: PLAY_TINT_HAIR[h1 % PLAY_TINT_HAIR.length], + body: PLAY_TINT_BODY[h2 % PLAY_TINT_BODY.length], + }; + } + + function hexToRgb01(hex) { + const h = (hex || '').replace('#', '').trim(); + if (h.length !== 6) return [1, 1, 1]; + return [ + parseInt(h.slice(0, 2), 16) / 255, + parseInt(h.slice(2, 4), 16) / 255, + parseInt(h.slice(4, 6), 16) / 255, + ]; + } + + function tintPlayLayerImageData(imageData, tintHex) { + const rgb = hexToRgb01(tintHex); + const tr = rgb[0], tg = rgb[1], tb = rgb[2]; + const d = imageData.data; + for (let i = 0; i < d.length; i += 4) { + if (d[i + 3] < 12) continue; + const r = d[i], g = d[i + 1], b = d[i + 2]; + const L = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + d[i] = Math.min(255, Math.round(tr * 255 * L)); + d[i + 1] = Math.min(255, Math.round(tg * 255 * L)); + d[i + 2] = Math.min(255, Math.round(tb * 255 * L)); + } + } + + function drawPlayTintedLayer(ctx, w, h, img, tintHex) { + if (!tintHex) { + ctx.drawImage(img, 0, 0, w, h); + return; + } + const c = document.createElement('canvas'); + c.width = w; + c.height = h; + const x = c.getContext('2d'); + x.drawImage(img, 0, 0, w, h); + try { + const idata = x.getImageData(0, 0, w, h); + tintPlayLayerImageData(idata, tintHex); + x.putImageData(idata, 0, 0); + } catch (e) { + x.clearRect(0, 0, w, h); + x.drawImage(img, 0, 0, w, h); + } + ctx.drawImage(c, 0, 0, w, h); + } + + function ensurePlayLayerImage(url) { + let img = playLayerImageCache.get(url); + if (!img) { + img = new Image(); + img.src = url; + playLayerImageCache.set(url, img); + } + return img; + } + + /** ลำดับลอง URL: เฟรมปัจจุบันแบบ multi → เฟรม 0 → แบบ single (ไม่มี _0_) เพื่อกันชื่อไฟล์ไม่ตรงกับที่เซิร์ฟเวอร์เขียน */ + function layerUrlCandidates(id, dir, layerName, frameIndex) { + const enc = encodeURIComponent(id); + const base = BASE + '/img/characters/' + enc + '_' + dir; + const out = []; + function add(u) { + if (out.indexOf(u) === -1) out.push(u); + } + add(base + '_' + frameIndex + '_layer_' + layerName + '.png'); + if (frameIndex !== 0) add(base + '_0_layer_' + layerName + '.png'); + add(base + '_layer_' + layerName + '.png'); + return out; + } + + /** เลเยอร์ idle: id_